mirror of
https://github.com/langgenius/dify.git
synced 2026-10-28 03:35:04 +08:00
refactor(web): migrate simple overlay tooltips (#35588)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
@@ -6,15 +6,13 @@ vi.mock('../../../base/icons/src/vender/line/files', () => ({
|
||||
CopyCheck: () => <span data-testid="copy-check-icon" />,
|
||||
}))
|
||||
|
||||
vi.mock('../../../base/tooltip', () => ({
|
||||
default: ({ children, popupContent }: { children: React.ReactNode, popupContent: string }) => (
|
||||
<div data-testid="tooltip" data-content={popupContent}>{children}</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/action-button', () => ({
|
||||
default: ({ children, onClick }: { children: React.ReactNode, onClick: () => void }) => (
|
||||
<button data-testid="action-button" onClick={onClick}>{children}</button>
|
||||
default: ({
|
||||
children,
|
||||
onClick,
|
||||
...props
|
||||
}: React.ButtonHTMLAttributes<HTMLButtonElement>) => (
|
||||
<button data-testid="action-button" onClick={onClick} {...props}>{children}</button>
|
||||
),
|
||||
}))
|
||||
|
||||
@@ -54,6 +52,6 @@ describe('KeyValueItem', () => {
|
||||
|
||||
it('renders copy tooltip', () => {
|
||||
render(<KeyValueItem label="ID" value="123" />)
|
||||
expect(screen.getByTestId('tooltip')).toHaveAttribute('data-content', 'common.operation.copy')
|
||||
expect(screen.getByRole('button', { name: 'common.operation.copy' })).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,24 +3,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { Theme } from '@/types/app'
|
||||
import IconWithTooltip from '../icon-with-tooltip'
|
||||
|
||||
// Mock Tooltip component
|
||||
vi.mock('@/app/components/base/tooltip', () => ({
|
||||
default: ({
|
||||
children,
|
||||
popupContent,
|
||||
popupClassName,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
popupContent?: string
|
||||
popupClassName?: string
|
||||
}) => (
|
||||
<div data-testid="tooltip" data-popup-content={popupContent} data-popup-classname={popupClassName}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock icon components
|
||||
const MockLightIcon = ({ className }: { className?: string }) => (
|
||||
<div data-testid="light-icon" className={className}>Light Icon</div>
|
||||
)
|
||||
@@ -44,10 +26,10 @@ describe('IconWithTooltip', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('tooltip')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('light-icon')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render Tooltip wrapper', () => {
|
||||
it('should render tooltip trigger with accessible label when popupContent is provided', () => {
|
||||
render(
|
||||
<IconWithTooltip
|
||||
theme={Theme.light}
|
||||
@@ -57,21 +39,7 @@ describe('IconWithTooltip', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('tooltip')).toHaveAttribute('data-popup-content', 'Test tooltip')
|
||||
})
|
||||
|
||||
it('should apply correct popupClassName to Tooltip', () => {
|
||||
render(
|
||||
<IconWithTooltip
|
||||
theme={Theme.light}
|
||||
BadgeIconLight={MockLightIcon}
|
||||
BadgeIconDark={MockDarkIcon}
|
||||
/>,
|
||||
)
|
||||
|
||||
const tooltip = screen.getByTestId('tooltip')
|
||||
expect(tooltip).toHaveAttribute('data-popup-classname')
|
||||
expect(tooltip.getAttribute('data-popup-classname')).toContain('border-components-panel-border')
|
||||
expect(screen.getByLabelText('Test tooltip')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -171,10 +139,7 @@ describe('IconWithTooltip', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('tooltip')).toHaveAttribute(
|
||||
'data-popup-content',
|
||||
'Custom tooltip content',
|
||||
)
|
||||
expect(screen.getByLabelText('Custom tooltip content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle undefined popupContent', () => {
|
||||
@@ -186,7 +151,7 @@ describe('IconWithTooltip', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('tooltip')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('light-icon')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -239,7 +204,7 @@ describe('IconWithTooltip', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('tooltip')).toHaveAttribute('data-popup-content', longContent)
|
||||
expect(screen.getByLabelText(longContent)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle special characters in popupContent', () => {
|
||||
@@ -253,7 +218,7 @@ describe('IconWithTooltip', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('tooltip')).toHaveAttribute('data-popup-content', specialContent)
|
||||
expect(screen.getByLabelText(specialContent)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { FC } from 'react'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import * as React from 'react'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { Theme } from '@/types/app'
|
||||
|
||||
type IconWithTooltipProps = {
|
||||
@@ -22,15 +22,24 @@ const IconWithTooltip: FC<IconWithTooltipProps> = ({
|
||||
const isDark = theme === Theme.dark
|
||||
const iconClassName = cn('h-5 w-5', className)
|
||||
const Icon = isDark ? BadgeIconDark : BadgeIconLight
|
||||
const icon = (
|
||||
<span
|
||||
aria-label={popupContent}
|
||||
className="flex shrink-0 items-center justify-center"
|
||||
>
|
||||
<Icon className={iconClassName} />
|
||||
</span>
|
||||
)
|
||||
|
||||
if (!popupContent)
|
||||
return icon
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
popupClassName="p-1.5 border-[0.5px] border-[0.5px] border-components-panel-border bg-components-tooltip-bg text-text-secondary system-xs-medium"
|
||||
popupContent={popupContent}
|
||||
>
|
||||
<div className="flex shrink-0 items-center justify-center">
|
||||
<Icon className={iconClassName} />
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={icon} />
|
||||
<TooltipContent className="border-[0.5px] border-components-panel-border bg-components-tooltip-bg p-1.5 system-xs-medium text-text-secondary">
|
||||
{popupContent}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
RiClipboardLine,
|
||||
} from '@remixicon/react'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import { CopyCheck } from '../../base/icons/src/vender/line/files'
|
||||
import Tooltip from '../../base/tooltip'
|
||||
|
||||
type Props = {
|
||||
label: string
|
||||
@@ -45,7 +42,7 @@ const KeyValueItem: FC<Props> = ({
|
||||
}
|
||||
}, [isCopied])
|
||||
|
||||
const CopyIcon = isCopied ? CopyCheck : RiClipboardLine
|
||||
const copyLabel = t(`operation.${isCopied ? 'copied' : 'copy'}`, { ns: 'common' })
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -54,10 +51,19 @@ const KeyValueItem: FC<Props> = ({
|
||||
<span className={cn(valueMaxWidthClassName, 'truncate system-xs-medium text-text-secondary')}>
|
||||
{maskedValue || value}
|
||||
</span>
|
||||
<Tooltip popupContent={t(`operation.${isCopied ? 'copied' : 'copy'}`, { ns: 'common' })} position="top">
|
||||
<ActionButton onClick={handleCopy}>
|
||||
<CopyIcon className="h-3.5 w-3.5 shrink-0 text-text-tertiary" />
|
||||
</ActionButton>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<ActionButton aria-label={copyLabel} onClick={handleCopy}>
|
||||
{isCopied
|
||||
? <CopyCheck aria-hidden className="h-3.5 w-3.5 shrink-0 text-text-tertiary" />
|
||||
: <span aria-hidden className="i-ri-clipboard-line h-3.5 w-3.5 shrink-0 text-text-tertiary" />}
|
||||
</ActionButton>
|
||||
)}
|
||||
/>
|
||||
<TooltipContent placement="top">
|
||||
{copyLabel}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+17
-57
@@ -3,14 +3,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { PluginSource } from '../../../../types'
|
||||
import PluginSourceBadge from '../plugin-source-badge'
|
||||
|
||||
vi.mock('@/app/components/base/tooltip', () => ({
|
||||
default: ({ children, popupContent }: { children: React.ReactNode, popupContent: string }) => (
|
||||
<div data-testid="tooltip" data-content={popupContent}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
describe('PluginSourceBadge', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@@ -20,33 +12,25 @@ describe('PluginSourceBadge', () => {
|
||||
it('should render marketplace source badge', () => {
|
||||
render(<PluginSourceBadge source={PluginSource.marketplace} />)
|
||||
|
||||
const tooltip = screen.getByTestId('tooltip')
|
||||
expect(tooltip).toBeInTheDocument()
|
||||
expect(tooltip).toHaveAttribute('data-content', 'plugin.detailPanel.categoryTip.marketplace')
|
||||
expect(screen.getByLabelText('plugin.detailPanel.categoryTip.marketplace')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render github source badge', () => {
|
||||
render(<PluginSourceBadge source={PluginSource.github} />)
|
||||
|
||||
const tooltip = screen.getByTestId('tooltip')
|
||||
expect(tooltip).toBeInTheDocument()
|
||||
expect(tooltip).toHaveAttribute('data-content', 'plugin.detailPanel.categoryTip.github')
|
||||
expect(screen.getByLabelText('plugin.detailPanel.categoryTip.github')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render local source badge', () => {
|
||||
render(<PluginSourceBadge source={PluginSource.local} />)
|
||||
|
||||
const tooltip = screen.getByTestId('tooltip')
|
||||
expect(tooltip).toBeInTheDocument()
|
||||
expect(tooltip).toHaveAttribute('data-content', 'plugin.detailPanel.categoryTip.local')
|
||||
expect(screen.getByLabelText('plugin.detailPanel.categoryTip.local')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render debugging source badge', () => {
|
||||
render(<PluginSourceBadge source={PluginSource.debugging} />)
|
||||
|
||||
const tooltip = screen.getByTestId('tooltip')
|
||||
expect(tooltip).toBeInTheDocument()
|
||||
expect(tooltip).toHaveAttribute('data-content', 'plugin.detailPanel.categoryTip.debugging')
|
||||
expect(screen.getByLabelText('plugin.detailPanel.categoryTip.debugging')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -86,71 +70,47 @@ describe('PluginSourceBadge', () => {
|
||||
it('should show marketplace tooltip', () => {
|
||||
render(<PluginSourceBadge source={PluginSource.marketplace} />)
|
||||
|
||||
expect(screen.getByTestId('tooltip')).toHaveAttribute(
|
||||
'data-content',
|
||||
'plugin.detailPanel.categoryTip.marketplace',
|
||||
)
|
||||
expect(screen.getByLabelText('plugin.detailPanel.categoryTip.marketplace')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show github tooltip', () => {
|
||||
render(<PluginSourceBadge source={PluginSource.github} />)
|
||||
|
||||
expect(screen.getByTestId('tooltip')).toHaveAttribute(
|
||||
'data-content',
|
||||
'plugin.detailPanel.categoryTip.github',
|
||||
)
|
||||
expect(screen.getByLabelText('plugin.detailPanel.categoryTip.github')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show local tooltip', () => {
|
||||
render(<PluginSourceBadge source={PluginSource.local} />)
|
||||
|
||||
expect(screen.getByTestId('tooltip')).toHaveAttribute(
|
||||
'data-content',
|
||||
'plugin.detailPanel.categoryTip.local',
|
||||
)
|
||||
expect(screen.getByLabelText('plugin.detailPanel.categoryTip.local')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show debugging tooltip', () => {
|
||||
render(<PluginSourceBadge source={PluginSource.debugging} />)
|
||||
|
||||
expect(screen.getByTestId('tooltip')).toHaveAttribute(
|
||||
'data-content',
|
||||
'plugin.detailPanel.categoryTip.debugging',
|
||||
)
|
||||
expect(screen.getByLabelText('plugin.detailPanel.categoryTip.debugging')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Icon Element Structure', () => {
|
||||
it('should render icon inside tooltip for marketplace', () => {
|
||||
render(<PluginSourceBadge source={PluginSource.marketplace} />)
|
||||
|
||||
const tooltip = screen.getByTestId('tooltip')
|
||||
const iconWrapper = tooltip.querySelector('div')
|
||||
expect(iconWrapper).toBeInTheDocument()
|
||||
const { container } = render(<PluginSourceBadge source={PluginSource.marketplace} />)
|
||||
expect(container.querySelector('[aria-label="plugin.detailPanel.categoryTip.marketplace"]')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render icon inside tooltip for github', () => {
|
||||
render(<PluginSourceBadge source={PluginSource.github} />)
|
||||
|
||||
const tooltip = screen.getByTestId('tooltip')
|
||||
const iconWrapper = tooltip.querySelector('div')
|
||||
expect(iconWrapper).toBeInTheDocument()
|
||||
const { container } = render(<PluginSourceBadge source={PluginSource.github} />)
|
||||
expect(container.querySelector('[aria-label="plugin.detailPanel.categoryTip.github"]')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render icon inside tooltip for local', () => {
|
||||
render(<PluginSourceBadge source={PluginSource.local} />)
|
||||
|
||||
const tooltip = screen.getByTestId('tooltip')
|
||||
const iconWrapper = tooltip.querySelector('div')
|
||||
expect(iconWrapper).toBeInTheDocument()
|
||||
const { container } = render(<PluginSourceBadge source={PluginSource.local} />)
|
||||
expect(container.querySelector('[aria-label="plugin.detailPanel.categoryTip.local"]')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render icon inside tooltip for debugging', () => {
|
||||
render(<PluginSourceBadge source={PluginSource.debugging} />)
|
||||
|
||||
const tooltip = screen.getByTestId('tooltip')
|
||||
const iconWrapper = tooltip.querySelector('div')
|
||||
expect(iconWrapper).toBeInTheDocument()
|
||||
const { container } = render(<PluginSourceBadge source={PluginSource.debugging} />)
|
||||
expect(container.querySelector('[aria-label="plugin.detailPanel.categoryTip.debugging"]')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -188,7 +148,7 @@ describe('PluginSourceBadge', () => {
|
||||
const invalidSource = '' as PluginSource
|
||||
render(<PluginSourceBadge source={invalidSource} />)
|
||||
|
||||
expect(screen.queryByTestId('tooltip')).not.toBeInTheDocument()
|
||||
expect(screen.queryByLabelText(/^plugin\.detailPanel\.categoryTip\./)).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
+15
-9
@@ -1,14 +1,10 @@
|
||||
'use client'
|
||||
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import {
|
||||
RiBugLine,
|
||||
RiHardDrive3Line,
|
||||
} from '@remixicon/react'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Github } from '@/app/components/base/icons/src/public/common'
|
||||
import { BoxSparkleFill } from '@/app/components/base/icons/src/vender/plugin'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { PluginSource } from '../../../types'
|
||||
|
||||
type SourceConfig = {
|
||||
@@ -30,11 +26,11 @@ const SOURCE_CONFIG_MAP: Record<PluginSource, SourceConfig | null> = {
|
||||
tipKey: 'detailPanel.categoryTip.github',
|
||||
},
|
||||
[PluginSource.local]: {
|
||||
icon: <RiHardDrive3Line className="h-3.5 w-3.5 text-text-tertiary" />,
|
||||
icon: <span aria-hidden className="i-ri-hard-drive-3-line h-3.5 w-3.5 text-text-tertiary" />,
|
||||
tipKey: 'detailPanel.categoryTip.local',
|
||||
},
|
||||
[PluginSource.debugging]: {
|
||||
icon: <RiBugLine className="h-3.5 w-3.5 text-text-tertiary hover:text-text-warning" />,
|
||||
icon: <span aria-hidden className="i-ri-bug-line h-3.5 w-3.5 text-text-tertiary hover:text-text-warning" />,
|
||||
tipKey: 'detailPanel.categoryTip.debugging',
|
||||
},
|
||||
}
|
||||
@@ -45,12 +41,22 @@ const PluginSourceBadge: FC<PluginSourceBadgeProps> = ({ source }) => {
|
||||
const config = SOURCE_CONFIG_MAP[source]
|
||||
if (!config)
|
||||
return null
|
||||
const tip = t(config.tipKey as never, { ns: 'plugin' })
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mr-0.5 ml-1 system-xs-regular text-text-quaternary">·</div>
|
||||
<Tooltip popupContent={t(config.tipKey as never, { ns: 'plugin' })}>
|
||||
<div>{config.icon}</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<span aria-label={tip} className="inline-flex">
|
||||
{config.icon}
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
<TooltipContent>
|
||||
{tip}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ComponentProps } from 'react'
|
||||
import type { EndpointListItem, PluginDetail } from '../types'
|
||||
import {
|
||||
AlertDialog,
|
||||
@@ -9,7 +10,7 @@ import {
|
||||
} from '@langgenius/dify-ui/alert-dialog'
|
||||
import { Switch } from '@langgenius/dify-ui/switch'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { RiClipboardLine, RiDeleteBinLine, RiEditLine, RiLoginCircleLine } from '@remixicon/react'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import * as React from 'react'
|
||||
@@ -17,7 +18,6 @@ import { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import { CopyCheck } from '@/app/components/base/icons/src/vender/line/files'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import { addDefaultValue, toolCredentialToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
|
||||
import {
|
||||
@@ -29,6 +29,8 @@ import {
|
||||
import EndpointModal from './endpoint-modal'
|
||||
import { NAME_FIELD } from './utils'
|
||||
|
||||
type EndpointModalFormSchemas = ComponentProps<typeof EndpointModal>['formSchemas']
|
||||
|
||||
type Props = {
|
||||
pluginDetail: PluginDetail
|
||||
data: EndpointListItem
|
||||
@@ -118,7 +120,7 @@ const EndpointCard = ({
|
||||
toast.error(t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }))
|
||||
},
|
||||
})
|
||||
const handleUpdate = (state: Record<string, any>) => updateEndpoint({
|
||||
const handleUpdate = (state: Record<string, unknown>) => updateEndpoint({
|
||||
endpointID,
|
||||
state,
|
||||
})
|
||||
@@ -148,22 +150,22 @@ const EndpointCard = ({
|
||||
}
|
||||
}, [isCopied])
|
||||
|
||||
const CopyIcon = isCopied ? CopyCheck : RiClipboardLine
|
||||
const copyLabel = t(`operation.${isCopied ? 'copied' : 'copy'}`, { ns: 'common' })
|
||||
|
||||
return (
|
||||
<div className="rounded-xl bg-background-section-burn p-0.5">
|
||||
<div className="group rounded-[10px] border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg p-2.5 pl-3">
|
||||
<div className="flex items-center">
|
||||
<div className="mb-1 flex h-6 grow items-center gap-1 system-md-semibold text-text-secondary">
|
||||
<RiLoginCircleLine className="h-4 w-4" />
|
||||
<span aria-hidden className="i-ri-login-circle-line h-4 w-4" />
|
||||
<div>{data.name}</div>
|
||||
</div>
|
||||
<div className="hidden items-center group-hover:flex">
|
||||
<ActionButton onClick={showEndpointModalConfirm}>
|
||||
<RiEditLine className="h-4 w-4" />
|
||||
<span aria-hidden className="i-ri-edit-line h-4 w-4" />
|
||||
</ActionButton>
|
||||
<ActionButton onClick={showDeleteConfirm} className="text-text-tertiary hover:bg-state-destructive-hover hover:text-text-destructive">
|
||||
<RiDeleteBinLine className="h-4 w-4" />
|
||||
<span aria-hidden className="i-ri-delete-bin-line h-4 w-4" />
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
@@ -172,10 +174,23 @@ const EndpointCard = ({
|
||||
<div className="w-12 shrink-0 system-xs-regular text-text-tertiary">{endpoint.method}</div>
|
||||
<div className="group/item flex grow items-center truncate system-xs-regular text-text-secondary">
|
||||
<div title={`${data.url}${endpoint.path}`} className="truncate">{`${data.url}${endpoint.path}`}</div>
|
||||
<Tooltip popupContent={t(`operation.${isCopied ? 'copied' : 'copy'}`, { ns: 'common' })} position="top">
|
||||
<ActionButton className="ml-2 hidden shrink-0 group-hover/item:flex" onClick={() => handleCopy(`${data.url}${endpoint.path}`)}>
|
||||
<CopyIcon className="h-3.5 w-3.5 text-text-tertiary" />
|
||||
</ActionButton>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<ActionButton
|
||||
aria-label={copyLabel}
|
||||
className="ml-2 hidden shrink-0 group-hover/item:flex"
|
||||
onClick={() => handleCopy(`${data.url}${endpoint.path}`)}
|
||||
>
|
||||
{isCopied
|
||||
? <CopyCheck aria-hidden className="h-3.5 w-3.5 text-text-tertiary" />
|
||||
: <span aria-hidden className="i-ri-clipboard-line h-3.5 w-3.5 text-text-tertiary" />}
|
||||
</ActionButton>
|
||||
)}
|
||||
/>
|
||||
<TooltipContent placement="top">
|
||||
{copyLabel}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
@@ -244,7 +259,7 @@ const EndpointCard = ({
|
||||
</AlertDialog>
|
||||
{isShowEndpointModal && (
|
||||
<EndpointModal
|
||||
formSchemas={formSchemas as any}
|
||||
formSchemas={formSchemas as EndpointModalFormSchemas}
|
||||
defaultValues={formValue}
|
||||
onCancel={hideEndpointModalConfirm}
|
||||
onSaved={handleUpdate}
|
||||
|
||||
+4
-12
@@ -7,12 +7,6 @@ vi.mock('@/app/components/base/progress-bar/progress-circle', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/tooltip', () => ({
|
||||
default: ({ children, popupContent }: { children: React.ReactNode, popupContent: string }) => (
|
||||
<div data-testid="tooltip" data-tip={popupContent}>{children}</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/header/plugins-nav/downloading-icon', () => ({
|
||||
default: () => <span data-testid="downloading-icon" />,
|
||||
}))
|
||||
@@ -38,18 +32,17 @@ describe('TaskStatusIndicator', () => {
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
render(<TaskStatusIndicator {...defaultProps} />)
|
||||
expect(screen.getByTestId('tooltip')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'Installing plugins' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should pass tip to tooltip', () => {
|
||||
it('should use tip as the trigger accessible name', () => {
|
||||
render(<TaskStatusIndicator {...defaultProps} tip="My tip" />)
|
||||
expect(screen.getByTestId('tooltip')).toHaveAttribute('data-tip', 'My tip')
|
||||
expect(screen.getByRole('button', { name: 'My tip' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render install icon by default', () => {
|
||||
const { container } = render(<TaskStatusIndicator {...defaultProps} />)
|
||||
// RiInstallLine renders as svg
|
||||
expect(container.querySelector('svg')).toBeInTheDocument()
|
||||
expect(container.querySelector('.i-ri-install-line')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('downloading-icon')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -127,7 +120,6 @@ describe('TaskStatusIndicator', () => {
|
||||
totalPluginsLength={3}
|
||||
/>,
|
||||
)
|
||||
// RiCheckboxCircleFill is rendered as svg with text-text-success
|
||||
const successIcon = container.querySelector('.text-text-success')
|
||||
expect(successIcon).toBeInTheDocument()
|
||||
})
|
||||
|
||||
+55
-54
@@ -1,12 +1,8 @@
|
||||
import type { FC } from 'react'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
RiCheckboxCircleFill,
|
||||
RiErrorWarningFill,
|
||||
RiInstallLine,
|
||||
} from '@remixicon/react'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import ProgressCircle from '@/app/components/base/progress-bar/progress-circle'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import DownloadingIcon from '@/app/components/header/plugins-nav/downloading-icon'
|
||||
|
||||
type TaskStatusIndicatorProps = {
|
||||
@@ -39,56 +35,61 @@ const TaskStatusIndicator: FC<TaskStatusIndicatorProps> = ({
|
||||
const showSuccessIcon = isSuccess || (successPluginsLength > 0 && runningPluginsLength === 0)
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
popupContent={tip}
|
||||
asChild
|
||||
offset={8}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex h-8 w-8 items-center justify-center rounded-lg border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg shadow-xs hover:bg-components-button-secondary-bg-hover',
|
||||
showErrorStyle && 'cursor-pointer border-components-button-destructive-secondary-border-hover bg-state-destructive-hover hover:bg-state-destructive-hover-alt',
|
||||
(isInstalling || isInstallingWithSuccess || isSuccess) && 'cursor-pointer hover:bg-components-button-secondary-bg-hover',
|
||||
)}
|
||||
id="plugin-task-trigger"
|
||||
onClick={onClick}
|
||||
>
|
||||
{/* Main Icon */}
|
||||
{showDownloadingIcon
|
||||
? <DownloadingIcon />
|
||||
: (
|
||||
<RiInstallLine
|
||||
className={cn(
|
||||
'h-4 w-4 text-components-button-secondary-text',
|
||||
showErrorStyle && 'text-components-button-destructive-secondary-text',
|
||||
)}
|
||||
/>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="small"
|
||||
aria-label={tip}
|
||||
className={cn(
|
||||
'relative h-8 w-8 rounded-lg px-0',
|
||||
'focus-visible:ring-2 focus-visible:ring-state-accent-solid',
|
||||
showErrorStyle && 'cursor-pointer border-components-button-destructive-secondary-border-hover bg-state-destructive-hover hover:bg-state-destructive-hover-alt',
|
||||
(isInstalling || isInstallingWithSuccess || isSuccess) && 'cursor-pointer hover:bg-components-button-secondary-bg-hover',
|
||||
)}
|
||||
id="plugin-task-trigger"
|
||||
onClick={onClick}
|
||||
>
|
||||
{showDownloadingIcon
|
||||
? <DownloadingIcon />
|
||||
: (
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn(
|
||||
'i-ri-install-line h-4 w-4 text-components-button-secondary-text',
|
||||
showErrorStyle && 'text-components-button-destructive-secondary-text',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Status Indicator Badge */}
|
||||
<div className="absolute -top-1 -right-1">
|
||||
{(isInstalling || isInstallingWithSuccess) && (
|
||||
<ProgressCircle
|
||||
percentage={(totalPluginsLength > 0 ? successPluginsLength / totalPluginsLength : 0) * 100}
|
||||
circleFillColor="fill-components-progress-brand-bg"
|
||||
/>
|
||||
)}
|
||||
{isInstallingWithError && (
|
||||
<ProgressCircle
|
||||
percentage={(totalPluginsLength > 0 ? runningPluginsLength / totalPluginsLength : 0) * 100}
|
||||
circleFillColor="fill-components-progress-brand-bg"
|
||||
sectorFillColor="fill-components-progress-error-border"
|
||||
circleStrokeColor="stroke-components-progress-error-border"
|
||||
/>
|
||||
)}
|
||||
{showSuccessIcon && !isInstalling && !isInstallingWithSuccess && !isInstallingWithError && (
|
||||
<RiCheckboxCircleFill className="h-3.5 w-3.5 text-text-success" />
|
||||
)}
|
||||
{isFailed && (
|
||||
<RiErrorWarningFill className="h-3.5 w-3.5 text-text-destructive" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute -top-1 -right-1">
|
||||
{(isInstalling || isInstallingWithSuccess) && (
|
||||
<ProgressCircle
|
||||
percentage={(totalPluginsLength > 0 ? successPluginsLength / totalPluginsLength : 0) * 100}
|
||||
circleFillColor="fill-components-progress-brand-bg"
|
||||
/>
|
||||
)}
|
||||
{isInstallingWithError && (
|
||||
<ProgressCircle
|
||||
percentage={(totalPluginsLength > 0 ? runningPluginsLength / totalPluginsLength : 0) * 100}
|
||||
circleFillColor="fill-components-progress-brand-bg"
|
||||
sectorFillColor="fill-components-progress-error-border"
|
||||
circleStrokeColor="stroke-components-progress-error-border"
|
||||
/>
|
||||
)}
|
||||
{showSuccessIcon && !isInstalling && !isInstallingWithSuccess && !isInstallingWithError && (
|
||||
<span aria-hidden className="i-ri-checkbox-circle-fill h-3.5 w-3.5 text-text-success" />
|
||||
)}
|
||||
{isFailed && (
|
||||
<span aria-hidden className="i-ri-error-warning-fill h-3.5 w-3.5 text-text-destructive" />
|
||||
)}
|
||||
</div>
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
<TooltipContent sideOffset={8}>{tip}</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user