Merge branch 'main' into jzh

This commit is contained in:
JzoNg 2026-04-27 13:48:47 +08:00
commit 89163edd16
24 changed files with 343 additions and 415 deletions

View File

@ -110,6 +110,8 @@ jobs:
- name: Web tsslint
if: steps.changed-files.outputs.any_changed == 'true'
working-directory: ./web
env:
NODE_OPTIONS: --max-old-space-size=4096
run: vp run lint:tss
- name: Web type check

View File

@ -2422,21 +2422,11 @@
"count": 1
}
},
"web/app/components/datasets/documents/create-from-pipeline/data-source/base/header.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"web/app/components/datasets/documents/create-from-pipeline/data-source/online-documents/index.tsx": {
"ts/no-explicit-any": {
"count": 1
}
},
"web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/header/breadcrumbs/bucket.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/list/item.tsx": {
"no-restricted-imports": {
"count": 1
@ -2525,11 +2515,6 @@
"count": 1
}
},
"web/app/components/datasets/documents/detail/completed/common/summary-status.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"web/app/components/datasets/documents/detail/completed/components/index.ts": {
"no-barrel-files/no-barrel-files": {
"count": 3
@ -2789,11 +2774,6 @@
"count": 2
}
},
"web/app/components/develop/secret-key/input-copy.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"web/app/components/develop/secret-key/secret-key-generate.tsx": {
"no-restricted-imports": {
"count": 1
@ -3159,16 +3139,6 @@
"count": 1
}
},
"web/app/components/plugins/base/badges/icon-with-tooltip.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"web/app/components/plugins/base/key-value-item.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"web/app/components/plugins/card/index.tsx": {
"ts/no-non-null-asserted-optional-chain": {
"count": 1
@ -3328,24 +3298,11 @@
"count": 2
}
},
"web/app/components/plugins/plugin-detail-panel/detail-header/components/plugin-source-badge.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"web/app/components/plugins/plugin-detail-panel/detail-header/hooks/index.ts": {
"no-barrel-files/no-barrel-files": {
"count": 3
}
},
"web/app/components/plugins/plugin-detail-panel/endpoint-card.tsx": {
"no-restricted-imports": {
"count": 1
},
"ts/no-explicit-any": {
"count": 2
}
},
"web/app/components/plugins/plugin-detail-panel/endpoint-list.tsx": {
"no-restricted-imports": {
"count": 1
@ -3544,11 +3501,6 @@
"count": 1
}
},
"web/app/components/plugins/plugin-page/plugin-tasks/components/task-status-indicator.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"web/app/components/plugins/readme-panel/index.tsx": {
"react/unsupported-syntax": {
"count": 1
@ -3822,14 +3774,6 @@
"count": 1
}
},
"web/app/components/tools/mcp/detail/content.tsx": {
"no-restricted-imports": {
"count": 1
},
"ts/no-explicit-any": {
"count": 3
}
},
"web/app/components/tools/mcp/detail/tool-item.tsx": {
"no-restricted-imports": {
"count": 1
@ -5394,14 +5338,6 @@
"count": 2
}
},
"web/app/components/workflow/panel/chat-variable-panel/components/variable-type-select.tsx": {
"no-restricted-imports": {
"count": 1
},
"ts/no-explicit-any": {
"count": 4
}
},
"web/app/components/workflow/panel/chat-variable-panel/type.ts": {
"erasable-syntax-only/enums": {
"count": 1

View File

@ -231,10 +231,8 @@ describe('Select wrappers', () => {
</Select>,
)
screen.getByRole('group', { name: 'select positioner' }).element().dispatchEvent(new MouseEvent('mouseover', {
bubbles: true,
}))
asHTMLElement(screen.getByRole('dialog', { name: 'select popup' }).element()).click()
await screen.getByRole('group', { name: 'select positioner' }).hover()
await screen.getByRole('dialog', { name: 'select popup' }).click()
screen.getByRole('listbox', { name: 'select list' }).element().dispatchEvent(new FocusEvent('focusin', {
bubbles: true,
}))

View File

@ -2,18 +2,10 @@ import { render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import Header from '../header'
vi.mock('@langgenius/dify-ui/button', () => ({
Button: ({ children }: { children: React.ReactNode }) => <button>{children}</button>,
}))
vi.mock('@/app/components/base/divider', () => ({
default: () => <span data-testid="divider" />,
}))
vi.mock('@/app/components/base/tooltip', () => ({
default: ({ children }: { children: React.ReactNode }) => <div data-testid="tooltip">{children}</div>,
}))
vi.mock('../credential-selector', () => ({
default: () => <div data-testid="credential-selector" />,
}))

View File

@ -1,10 +1,9 @@
import type { CredentialSelectorProps } from './credential-selector'
import { Button } from '@langgenius/dify-ui/button'
import { RiBookOpenLine, RiEqualizer2Line } from '@remixicon/react'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import Divider from '@/app/components/base/divider'
import Tooltip from '@/app/components/base/tooltip'
import CredentialSelector from './credential-selector'
type HeaderProps = {
@ -22,6 +21,7 @@ const Header = ({
...rest
}: HeaderProps) => {
const { t } = useTranslation()
const configurationTip = t('configurationTip', { ns: 'datasetPipeline', pluginName })
return (
<div className="flex items-center justify-between gap-x-2">
@ -30,20 +30,23 @@ const Header = ({
{...rest}
/>
<Divider type="vertical" className="mx-1 h-3.5 shrink-0" />
<Tooltip
popupContent={t('configurationTip', { ns: 'datasetPipeline', pluginName })}
position="top"
>
<Button
variant="ghost"
size="small"
className="size-6 shrink-0 px-1"
>
<RiEqualizer2Line
className="h-4 w-4"
onClick={onClickConfiguration}
/>
</Button>
<Tooltip>
<TooltipTrigger
render={(
<Button
variant="ghost"
size="small"
className="size-6 shrink-0 px-1"
aria-label={configurationTip}
onClick={onClickConfiguration}
>
<span aria-hidden className="i-ri-equalizer-2-line h-4 w-4" />
</Button>
)}
/>
<TooltipContent>
{configurationTip}
</TooltipContent>
</Tooltip>
</div>
<a
@ -52,7 +55,7 @@ const Header = ({
target="_blank"
rel="noopener noreferrer"
>
<RiBookOpenLine className="size-3.5 shrink-0" />
<span aria-hidden className="i-ri-book-open-line size-3.5 shrink-0" />
<span title={docTitle}>{docTitle}</span>
</a>
</div>

View File

@ -5,9 +5,6 @@ import Bucket from '../bucket'
vi.mock('@/app/components/base/icons/src/public/knowledge/online-drive', () => ({
BucketsGray: (props: React.SVGProps<SVGSVGElement>) => <svg data-testid="buckets-gray" {...props} />,
}))
vi.mock('@/app/components/base/tooltip', () => ({
default: ({ children }: { children?: React.ReactNode }) => <div>{children}</div>,
}))
describe('Bucket', () => {
const defaultProps = {
@ -32,8 +29,7 @@ describe('Bucket', () => {
it('should call handleBackToBucketList on icon button click', () => {
render(<Bucket {...defaultProps} />)
const buttons = screen.getAllByRole('button')
fireEvent.click(buttons[0]!)
fireEvent.click(screen.getByRole('button', { name: 'datasetPipeline.onlineDrive.breadcrumbs.allBuckets' }))
expect(defaultProps.handleBackToBucketList).toHaveBeenCalledOnce()
})

View File

@ -1,9 +1,10 @@
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import * as React from 'react'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { BucketsGray } from '@/app/components/base/icons/src/public/knowledge/online-drive'
import Tooltip from '@/app/components/base/tooltip'
type BucketProps = {
bucketName: string
@ -27,19 +28,28 @@ const Bucket = ({
if (!disabled)
handleClickBucketName()
}, [disabled, handleClickBucketName])
const allBucketsLabel = t('onlineDrive.breadcrumbs.allBuckets', { ns: 'datasetPipeline' })
return (
<>
<Tooltip
popupContent={t('onlineDrive.breadcrumbs.allBuckets', { ns: 'datasetPipeline' })}
>
<button
type="button"
className="flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-md hover:bg-state-base-hover"
onClick={handleBackToBucketList}
>
<BucketsGray />
</button>
<Tooltip>
<TooltipTrigger
render={(
<Button
type="button"
variant="ghost"
size="small"
aria-label={allBucketsLabel}
className="size-6 shrink-0 rounded-md px-0 hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid"
onClick={handleBackToBucketList}
>
<BucketsGray aria-hidden />
</Button>
)}
/>
<TooltipContent>
{allBucketsLabel}
</TooltipContent>
</Tooltip>
<span className="system-xs-regular text-divider-deep">/</span>
<button

View File

@ -9,9 +9,6 @@ vi.mock('@/app/components/base/badge', () => ({
vi.mock('@/app/components/base/icons/src/vender/knowledge', () => ({
SearchLinesSparkle: (props: React.SVGProps<SVGSVGElement>) => <svg data-testid="sparkle-icon" {...props} />,
}))
vi.mock('@/app/components/base/tooltip', () => ({
default: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}))
describe('SummaryStatus', () => {
it('should render badge for SUMMARIZING status', () => {

View File

@ -1,8 +1,8 @@
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { memo, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import Badge from '@/app/components/base/badge'
import { SearchLinesSparkle } from '@/app/components/base/icons/src/vender/knowledge'
import Tooltip from '@/app/components/base/tooltip'
type SummaryStatusProps = {
status: string
@ -18,18 +18,22 @@ const SummaryStatus = ({ status }: SummaryStatusProps) => {
return ''
}, [status, t])
if (status !== 'SUMMARIZING')
return null
return (
<Tooltip
popupContent={tip}
>
{
status === 'SUMMARIZING' && (
<Badge className="border-text-accent-secondary text-text-accent-secondary">
<SearchLinesSparkle className="mr-0.5 h-3 w-3" />
<span>{t('list.summary.generating', { ns: 'datasetDocuments' })}</span>
</Badge>
)
}
<Tooltip>
<TooltipTrigger
render={(
<span className="inline-flex">
<Badge className="border-text-accent-secondary text-text-accent-secondary">
<SearchLinesSparkle aria-hidden className="mr-0.5 h-3 w-3" />
<span>{t('list.summary.generating', { ns: 'datasetDocuments' })}</span>
</Badge>
</span>
)}
/>
<TooltipContent>{tip}</TooltipContent>
</Tooltip>
)
}

View File

@ -35,7 +35,7 @@ describe('InputCopy', () => {
it('should render with empty value by default', async () => {
await renderAndFlush(<InputCopy />)
expect(screen.getByRole('button')).toBeInTheDocument()
expect(screen.getAllByRole('button').length).toBeGreaterThan(0)
})
it('should render children when provided', async () => {
@ -273,12 +273,12 @@ describe('InputCopy', () => {
describe('edge cases', () => {
it('should handle undefined value', async () => {
await renderAndFlush(<InputCopy value={undefined} />)
expect(screen.getByRole('button')).toBeInTheDocument()
expect(screen.getAllByRole('button').length).toBeGreaterThan(0)
})
it('should handle empty string value', async () => {
await renderAndFlush(<InputCopy value="" />)
expect(screen.getByRole('button')).toBeInTheDocument()
expect(screen.getAllByRole('button').length).toBeGreaterThan(0)
})
it('should handle very long values', async () => {

View File

@ -1,9 +1,9 @@
'use client'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { t } from 'i18next'
import * as React from 'react'
import { useEffect, useState } from 'react'
import CopyFeedback from '@/app/components/base/copy-feedback'
import Tooltip from '@/app/components/base/tooltip'
import { writeTextToClipboard } from '@/utils/clipboard'
type IInputCopyProps = {
@ -18,6 +18,12 @@ const InputCopy = ({
children,
}: IInputCopyProps) => {
const [isCopied, setIsCopied] = useState(false)
const copyLabel = isCopied ? `${t('copied', { ns: 'appApi' })}` : `${t('copy', { ns: 'appApi' })}`
const handleCopy = () => {
writeTextToClipboard(value).then(() => {
setIsCopied(true)
})
}
useEffect(() => {
if (isCopied) {
@ -38,17 +44,24 @@ const InputCopy = ({
<div className="relative h-full grow text-[13px]">
<div
className="r-0 absolute top-0 left-0 w-full cursor-pointer truncate pr-2 pl-2"
onClick={() => {
writeTextToClipboard(value).then(() => {
setIsCopied(true)
})
role="button"
aria-label={copyLabel}
tabIndex={0}
onClick={handleCopy}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault()
handleCopy()
}
}}
>
<Tooltip
popupContent={isCopied ? `${t('copied', { ns: 'appApi' })}` : `${t('copy', { ns: 'appApi' })}`}
position="bottom"
>
<span className="text-text-secondary">{value}</span>
<Tooltip>
<TooltipTrigger
render={<span className="text-text-secondary">{value}</span>}
/>
<TooltipContent placement="bottom">
{copyLabel}
</TooltipContent>
</Tooltip>
</div>
</div>

View File

@ -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()
})
})

View File

@ -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()
})
})
})

View File

@ -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>
)
}

View File

@ -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>

View File

@ -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()
})
})
})

View File

@ -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>
</>
)

View File

@ -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}

View File

@ -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()
})

View File

@ -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>
)
}

View File

@ -698,16 +698,9 @@ describe('MCPDetailContent', () => {
const onHide = vi.fn()
render(<MCPDetailContent {...defaultProps} onHide={onHide} />, { wrapper: createWrapper() })
// Find the close button (ActionButton with RiCloseLine)
const buttons = screen.getAllByRole('button')
const closeButton = buttons.find(btn =>
btn.querySelector('svg.h-4.w-4'),
)
fireEvent.click(screen.getByRole('button', { name: 'common.operation.close' }))
if (closeButton) {
fireEvent.click(closeButton)
expect(onHide).toHaveBeenCalled()
}
expect(onHide).toHaveBeenCalled()
})
})

View File

@ -1,5 +1,5 @@
'use client'
import type { FC } from 'react'
import type { ComponentProps, FC } from 'react'
import type { ToolWithProvider } from '../../../workflow/types'
import {
AlertDialog,
@ -12,18 +12,13 @@ import {
} from '@langgenius/dify-ui/alert-dialog'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import {
RiCloseLine,
RiLoader2Line,
RiLoopLeftLine,
} 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'
import { useCallback, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import Tooltip from '@/app/components/base/tooltip'
import Indicator from '@/app/components/header/indicator'
import Icon from '@/app/components/plugins/card/base/card-icon'
import { useAppContext } from '@/context/app-context'
@ -49,6 +44,11 @@ type Props = {
onFirstCreate: () => void
}
type MCPModalConfirmPayload = Parameters<ComponentProps<typeof MCPModal>['onConfirm']>[0]
type MutationResult = {
result?: string
}
const MCPDetailContent: FC<Props> = ({
detail,
onUpdate,
@ -128,14 +128,14 @@ const MCPDetailContent: FC<Props> = ({
}
}, [onFirstCreate, isCurrentWorkspaceManager, detail, authorizeMcp, handleUpdateTools, handleOAuthCallback, onUpdate])
const handleUpdate = useCallback(async (data: any) => {
const handleUpdate = useCallback(async (data: MCPModalConfirmPayload) => {
if (!detail)
return
const res = await updateMCP({
...data,
provider_id: detail.id,
})
if ((res as any)?.result === 'success') {
}) as MutationResult
if (res.result === 'success') {
hideUpdateModal()
onUpdate()
handleAuthorize()
@ -146,9 +146,9 @@ const MCPDetailContent: FC<Props> = ({
if (!detail)
return
showDeleting()
const res = await deleteMCP(detail.id)
const res = await deleteMCP(detail.id) as MutationResult
hideDeleting()
if ((res as any)?.result === 'success') {
if (res.result === 'success') {
hideDeleteConfirm()
onUpdate(true)
}
@ -161,6 +161,8 @@ const MCPDetailContent: FC<Props> = ({
if (!detail)
return null
const identifierLabel = t('mcp.identifier', { ns: 'tools' })
const serverUrlLabel = t('mcp.modal.serverUrl', { ns: 'tools' })
return (
<>
@ -174,12 +176,37 @@ const MCPDetailContent: FC<Props> = ({
<div className="truncate system-md-semibold text-text-primary" title={detail.name}>{detail.name}</div>
</div>
<div className="mt-0.5 flex items-center gap-1">
<Tooltip popupContent={t('mcp.identifier', { ns: 'tools' })}>
<div className="shrink-0 cursor-pointer system-xs-regular text-text-secondary" onClick={() => copy(detail.server_identifier || '')}>{detail.server_identifier}</div>
<Tooltip>
<TooltipTrigger
render={(
<Button
type="button"
variant="ghost"
size="small"
aria-label={identifierLabel}
className="h-auto shrink-0 cursor-pointer rounded bg-transparent p-0 text-left system-xs-regular text-text-secondary hover:bg-transparent focus-visible:ring-2 focus-visible:ring-state-accent-solid"
onClick={() => copy(detail.server_identifier || '')}
>
{detail.server_identifier}
</Button>
)}
/>
<TooltipContent>
{identifierLabel}
</TooltipContent>
</Tooltip>
<div className="shrink-0 system-xs-regular text-text-quaternary">·</div>
<Tooltip popupContent={t('mcp.modal.serverUrl', { ns: 'tools' })}>
<div className="truncate system-xs-regular text-text-secondary">{detail.server_url}</div>
<Tooltip>
<TooltipTrigger
render={(
<div aria-label={serverUrlLabel} className="truncate system-xs-regular text-text-secondary">
{detail.server_url}
</div>
)}
/>
<TooltipContent>
{serverUrlLabel}
</TooltipContent>
</Tooltip>
</div>
</div>
@ -188,8 +215,8 @@ const MCPDetailContent: FC<Props> = ({
onEdit={showUpdateModal}
onRemove={showDeleteConfirm}
/>
<ActionButton onClick={onHide}>
<RiCloseLine className="h-4 w-4" />
<ActionButton aria-label={t('operation.close', { ns: 'common' })} onClick={onHide}>
<span aria-hidden className="i-ri-close-line h-4 w-4" />
</ActionButton>
</div>
</div>
@ -221,7 +248,7 @@ const MCPDetailContent: FC<Props> = ({
className="w-full"
disabled
>
<RiLoader2Line className={cn('mr-1 h-4 w-4 animate-spin')} />
<span aria-hidden className="mr-1 i-ri-loader-2-line h-4 w-4 animate-spin" />
{t('mcp.authorizing', { ns: 'tools' })}
</Button>
)}
@ -262,7 +289,7 @@ const MCPDetailContent: FC<Props> = ({
</div>
<div>
<Button size="small" onClick={showUpdateConfirm}>
<RiLoopLeftLine className="mr-1 h-3.5 w-3.5" />
<span aria-hidden className="mr-1 i-ri-loop-left-line h-3.5 w-3.5" />
{t('mcp.update', { ns: 'tools' })}
</Button>
</div>

View File

@ -36,8 +36,9 @@ describe('VariableTypeSelector', () => {
await user.keyboard('{Escape}')
await waitFor(() => {
expect(screen.queryByText('number')).not.toBeInTheDocument()
expect(screen.getByRole('combobox')).toHaveAttribute('aria-expanded', 'false')
})
expect(screen.queryByRole('listbox')).not.toBeInTheDocument()
})
it('keeps the custom popup class in in-cell mode', async () => {

View File

@ -1,38 +1,47 @@
'use client'
import { cn } from '@langgenius/dify-ui/cn'
import { RiArrowDownSLine, RiCheckLine } from '@remixicon/react'
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
import * as React from 'react'
import { useState } from 'react'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
type Props = {
type Props<T extends string> = {
inCell?: boolean
value?: any
list: any
onSelect: (value: any) => void
value?: T
list: readonly T[]
onSelect: (value: T) => void
popupClassName?: string
}
const VariableTypeSelector = ({
const VariableTypeSelector = <T extends string, >({
inCell = false,
value,
list,
onSelect,
popupClassName,
}: Props) => {
}: Props<T>) => {
const [open, setOpen] = useState(false)
const handleValueChange = (nextValue: string | null) => {
if (!nextValue)
return
const nextItem = list.find(item => item === nextValue)
if (!nextItem)
return
onSelect(nextItem)
}
return (
<PortalToFollowElem
<Select
value={value ?? null}
open={open}
onOpenChange={() => setOpen(v => !v)}
placement="bottom"
onOpenChange={setOpen}
onValueChange={handleValueChange}
>
<PortalToFollowElemTrigger className="w-full" onClick={() => setOpen(v => !v)}>
<SelectTrigger
className="h-auto w-full max-w-none cursor-pointer rounded-none bg-transparent p-0 hover:bg-transparent focus-visible:bg-transparent data-popup-open:bg-transparent [&>*:last-child]:hidden"
>
<div className={cn(
'flex w-full cursor-pointer items-center px-2',
!inCell && 'rounded-lg bg-components-input-bg-normal py-1 hover:bg-state-base-hover-alt',
@ -48,27 +57,22 @@ const VariableTypeSelector = ({
>
{value}
</div>
<RiArrowDownSLine className="ml-0.5 h-4 w-4 text-text-quaternary" />
<span className="ml-0.5 i-ri-arrow-down-s-line h-4 w-4 text-text-quaternary" aria-hidden="true" />
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className={cn('z-11 w-full', popupClassName)}>
<div className="rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg">
{list.map((item: any) => (
<div
key={item}
className="flex cursor-pointer items-center gap-2 rounded-lg py-[6px] pr-2 pl-3 hover:bg-state-base-hover"
onClick={() => {
onSelect(item)
setOpen(false)
}}
>
<div className="grow truncate system-md-regular text-text-secondary">{item}</div>
{value === item && <RiCheckLine className="h-4 w-4 text-text-accent" />}
</div>
))}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
</SelectTrigger>
<SelectContent placement="bottom-start" popupClassName={cn('bg-components-panel-bg-blur', popupClassName)}>
{list.map(item => (
<SelectItem
key={item}
value={item}
className="h-auto gap-2 py-[6px] pr-2 pl-3 system-md-regular font-normal"
>
<SelectItemText className="px-0 system-md-regular text-text-secondary">{item}</SelectItemText>
<SelectItemIndicator />
</SelectItem>
))}
</SelectContent>
</Select>
)
}