style: fix provider card dropdown menu seperator margin (#38422)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
yyh 2026-07-05 22:00:40 +08:00 committed by GitHub
parent 1b3e8e9943
commit 750075c859
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 245 additions and 326 deletions

View File

@ -3769,14 +3769,6 @@
"count": 2
}
},
"web/app/components/plugins/plugin-detail-panel/__tests__/operation-dropdown.spec.tsx": {
"jsx-a11y/click-events-have-key-events": {
"count": 1
},
"jsx-a11y/no-static-element-interactions": {
"count": 1
}
},
"web/app/components/plugins/plugin-detail-panel/agent-strategy-list.tsx": {
"ts/no-explicit-any": {
"count": 1

View File

@ -1,6 +1,7 @@
import type { ReactNode } from 'react'
import type { PluginDetail } from '@/app/components/plugins/types'
import { fireEvent, render, screen } from '@testing-library/react'
import { fireEvent, screen } from '@testing-library/react'
import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
import { PluginCategoryEnum, PluginSource } from '@/app/components/plugins/types'
import DataSourcePluginActions from '../plugin-actions'
@ -22,15 +23,6 @@ vi.mock('@/app/components/plugins/readme-panel/store', () => ({
}),
}))
vi.mock('@/app/components/plugins/plugin-detail-panel/operation-dropdown', () => ({
__esModule: true,
default: ({ onViewReadme }: { onViewReadme?: () => void }) => (
<button type="button" onClick={onViewReadme}>
plugin.detailPanel.operation.viewReadme
</button>
),
}))
vi.mock('@/app/components/plugins/plugin-detail-panel/detail-header/hooks', () => ({
useDetailHeaderState: () => ({
modalStates: {
@ -150,7 +142,10 @@ describe('DataSourcePluginActions', () => {
it('opens the plugin README from the actions menu', () => {
const detail = createPluginDetail()
render(<DataSourcePluginActions detail={detail} />)
renderWithSystemFeatures(<DataSourcePluginActions detail={detail} />, {
systemFeatures: { enable_marketplace: true },
})
fireEvent.click(screen.getByRole('button', { name: 'plugin.detailPanel.operation.moreActions' }))
fireEvent.click(screen.getByText('plugin.detailPanel.operation.viewReadme'))
expect(mockOpenReadmePanel).toHaveBeenCalledWith(expect.objectContaining({

View File

@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next'
import Badge from '@/app/components/base/badge'
import { HeaderModals } from '@/app/components/plugins/plugin-detail-panel/detail-header/components'
import { useDetailHeaderState, usePluginOperations } from '@/app/components/plugins/plugin-detail-panel/detail-header/hooks'
import OperationDropdown from '@/app/components/plugins/plugin-detail-panel/operation-dropdown'
import { OperationDropdown } from '@/app/components/plugins/plugin-detail-panel/operation-dropdown'
import { usePluginSettingsAccess } from '@/app/components/plugins/plugin-page/use-reference-setting'
import { useReadmePanelStore } from '@/app/components/plugins/readme-panel/store'
import { PluginSource } from '@/app/components/plugins/types'

View File

@ -1,6 +1,7 @@
import type { ReactNode } from 'react'
import type { ReactElement, ReactNode } from 'react'
import type { PluginDetail } from '@/app/components/plugins/types'
import { fireEvent, render, screen } from '@testing-library/react'
import { fireEvent, screen } from '@testing-library/react'
import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
import { PluginSource } from '@/app/components/plugins/types'
import ProviderCardActions from '../provider-card-actions'
@ -31,6 +32,13 @@ let mockHeaderState = {
isFromGitHub: false,
}
const render = (ui: ReactElement) =>
renderWithSystemFeatures(ui, { systemFeatures: { enable_marketplace: true } })
const openActionsMenu = () => {
fireEvent.click(screen.getByRole('button', { name: 'plugin.detailPanel.operation.moreActions' }))
}
vi.mock('@/app/components/plugins/plugin-detail-panel/detail-header/hooks', () => ({
useDetailHeaderState: () => mockHeaderState,
usePluginOperations: () => ({
@ -55,22 +63,6 @@ vi.mock('@/app/components/plugins/plugin-detail-panel/detail-header/components',
),
}))
vi.mock('@/app/components/plugins/plugin-detail-panel/operation-dropdown', () => ({
default: ({ detailUrl, onInfo, onCheckVersion, onRemove, destructiveRemove }: {
detailUrl: string
onInfo: () => void
onCheckVersion: () => void
onRemove: () => void
destructiveRemove?: boolean
}) => (
<div data-testid="operation-dropdown" data-detail-url={detailUrl} data-destructive-remove={String(Boolean(destructiveRemove))}>
<button type="button" onClick={onInfo}>info</button>
<button type="button" onClick={onCheckVersion}>check version</button>
<button type="button" onClick={onRemove}>remove</button>
</div>
),
}))
vi.mock('@/app/components/plugins/plugin-page/use-reference-setting', () => ({
usePluginSettingsAccess: () => ({
canDeletePlugin: true,
@ -185,27 +177,19 @@ describe('ProviderCardActions', () => {
language: 'en-US',
theme: 'light',
})
expect(screen.getByTestId('operation-dropdown')).toHaveAttribute(
'data-detail-url',
openActionsMenu()
expect(screen.getByRole('menuitem', { name: 'plugin.detailPanel.operation.viewDetail' })).toHaveAttribute(
'href',
'https://marketplace.example.com/plugins/langgenius/provider-plugin',
)
})
it('should request destructive remove styling for the operation dropdown', () => {
it('should relay the marketplace remove action', () => {
render(<ProviderCardActions detail={createDetail()} />)
expect(screen.getByTestId('operation-dropdown')).toHaveAttribute('data-destructive-remove', 'true')
})
openActionsMenu()
fireEvent.click(screen.getByText('plugin.detailPanel.operation.remove'))
it('should relay operation dropdown actions', () => {
render(<ProviderCardActions detail={createDetail()} />)
fireEvent.click(screen.getByRole('button', { name: 'info' }))
fireEvent.click(screen.getByRole('button', { name: 'check version' }))
fireEvent.click(screen.getByRole('button', { name: 'remove' }))
expect(mockShowPluginInfo).toHaveBeenCalledTimes(1)
expect(mockHandleUpdate).toHaveBeenCalledTimes(1)
expect(mockShowDeleteConfirm).toHaveBeenCalledTimes(1)
})
@ -230,10 +214,8 @@ describe('ProviderCardActions', () => {
)
expect(screen.getByTestId('plugin-version-picker')).toHaveAttribute('data-disabled', 'true')
expect(screen.getByTestId('operation-dropdown')).toHaveAttribute(
'data-detail-url',
'https://github.com/langgenius/provider-plugin',
)
openActionsMenu()
expect(screen.getByRole('menuitem', { name: 'plugin.detailPanel.operation.viewDetail' })).toHaveAttribute('href', 'https://github.com/langgenius/provider-plugin')
fireEvent.click(screen.getByRole('button', { name: 'plugin.detailPanel.operation.update' }))
@ -241,6 +223,38 @@ describe('ProviderCardActions', () => {
expect(mockHandleUpdate).toHaveBeenCalledWith()
})
it('should relay GitHub operation dropdown actions', () => {
mockHeaderState = {
...mockHeaderState,
hasNewVersion: false,
isFromMarketplace: false,
isFromGitHub: true,
}
render(
<ProviderCardActions detail={createDetail({
source: PluginSource.github,
meta: {
repo: 'langgenius/provider-plugin',
version: '1.0.0',
package: 'provider-plugin.difypkg',
},
})}
/>,
)
openActionsMenu()
fireEvent.click(screen.getByText('plugin.detailPanel.operation.info'))
openActionsMenu()
fireEvent.click(screen.getByText('plugin.detailPanel.operation.checkUpdate'))
openActionsMenu()
fireEvent.click(screen.getByText('plugin.detailPanel.operation.remove'))
expect(mockShowPluginInfo).toHaveBeenCalledTimes(1)
expect(mockHandleUpdate).toHaveBeenCalledTimes(1)
expect(mockShowDeleteConfirm).toHaveBeenCalledTimes(1)
})
it('should fall back to the detail name when declaration metadata is missing', () => {
render(
<ProviderCardActions
@ -266,7 +280,8 @@ describe('ProviderCardActions', () => {
/>,
)
expect(screen.getByTestId('operation-dropdown')).toHaveAttribute('data-detail-url', '')
openActionsMenu()
expect(screen.getByRole('menuitem', { name: 'plugin.detailPanel.operation.viewDetail' })).toHaveAttribute('href', '')
rerender(
<ProviderCardActions
@ -276,6 +291,7 @@ describe('ProviderCardActions', () => {
/>,
)
expect(screen.getByTestId('operation-dropdown')).toHaveAttribute('data-detail-url', '')
openActionsMenu()
expect(screen.queryByText('plugin.detailPanel.operation.viewDetail')).not.toBeInTheDocument()
})
})

View File

@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next'
import Badge from '@/app/components/base/badge'
import { HeaderModals } from '@/app/components/plugins/plugin-detail-panel/detail-header/components'
import { useDetailHeaderState, usePluginOperations } from '@/app/components/plugins/plugin-detail-panel/detail-header/hooks'
import OperationDropdown from '@/app/components/plugins/plugin-detail-panel/operation-dropdown'
import { OperationDropdown } from '@/app/components/plugins/plugin-detail-panel/operation-dropdown'
import { usePluginSettingsAccess } from '@/app/components/plugins/plugin-page/use-reference-setting'
import { PluginSource } from '@/app/components/plugins/types'
import PluginVersionPicker from '@/app/components/plugins/update-plugin/plugin-version-picker'

View File

@ -14,6 +14,15 @@ const render = (ui: ReactElement) =>
systemFeatures: { enable_marketplace: mockEnableMarketplace },
})
const openActionsMenu = () => {
fireEvent.click(screen.getByRole('button', { name: 'plugin.detailPanel.operation.moreActions' }))
}
const clickOperation = (label: string) => {
openActionsMenu()
fireEvent.click(screen.getByText(label))
}
const { mockToast } = vi.hoisted(() => ({
mockToast: Object.assign(vi.fn(), {
success: vi.fn(),
@ -211,28 +220,6 @@ vi.mock('../../base/deprecation-notice', () => ({
default: () => <div data-testid="deprecation-notice" />,
}))
// Enhanced operation-dropdown mock
vi.mock('../operation-dropdown', () => ({
default: ({
onInfo,
onCheckVersion,
onRemove,
onViewReadme,
}: {
onInfo: () => void
onCheckVersion: () => void
onRemove: () => void
onViewReadme?: () => void
}) => (
<div data-testid="operation-dropdown">
<button data-testid="info-btn" onClick={onInfo}>Info</button>
<button data-testid="check-version-btn" onClick={onCheckVersion}>Check Version</button>
<button data-testid="remove-btn" onClick={onRemove}>Remove</button>
{onViewReadme && <button data-testid="view-readme-btn" onClick={onViewReadme}>View README</button>}
</div>
),
}))
// Enhanced update modal mock
vi.mock('../../update-plugin/from-market-place', () => ({
default: ({ onSave, onCancel }: { onSave: () => void, onCancel: () => void }) => {
@ -577,22 +564,28 @@ describe('DetailHeader', () => {
expect(mockOnHide).toHaveBeenCalled()
})
it('should have info button available', () => {
render(<DetailHeader detail={createPluginDetail()} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
it('should expose info action for GitHub plugins', () => {
const detail = createPluginDetail({
source: PluginSource.github,
meta: { repo: 'owner/repo', version: 'v1.0.0', package: 'pkg' },
})
render(<DetailHeader detail={detail} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
const infoBtn = screen.getByTestId('info-btn')
fireEvent.click(infoBtn)
openActionsMenu()
expect(infoBtn)!.toBeInTheDocument()
expect(screen.getByText('plugin.detailPanel.operation.info')).toBeInTheDocument()
})
it('should have check version button available', () => {
render(<DetailHeader detail={createPluginDetail()} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
it('should expose check version action for GitHub plugins', () => {
const detail = createPluginDetail({
source: PluginSource.github,
meta: { repo: 'owner/repo', version: 'v1.0.0', package: 'pkg' },
})
render(<DetailHeader detail={detail} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
const checkBtn = screen.getByTestId('check-version-btn')
fireEvent.click(checkBtn)
openActionsMenu()
expect(checkBtn)!.toBeInTheDocument()
expect(screen.getByText('plugin.detailPanel.operation.checkUpdate')).toBeInTheDocument()
})
})
@ -698,18 +691,9 @@ describe('DetailHeader', () => {
it('should have remove button available', () => {
render(<DetailHeader detail={createPluginDetail()} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
const removeBtn = screen.getByTestId('remove-btn')
fireEvent.click(removeBtn)
openActionsMenu()
expect(removeBtn)!.toBeInTheDocument()
})
it('should have uninstallPlugin mock defined', () => {
render(<DetailHeader detail={createPluginDetail()} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
fireEvent.click(screen.getByTestId('remove-btn'))
expect(mockUninstallPlugin).toBeDefined()
expect(screen.getByText('plugin.detailPanel.operation.remove')).toBeInTheDocument()
})
it('should render correctly for model plugin delete', () => {
@ -721,13 +705,15 @@ describe('DetailHeader', () => {
})
render(<DetailHeader detail={detail} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
expect(screen.getByTestId('remove-btn'))!.toBeInTheDocument()
openActionsMenu()
expect(screen.getByText('plugin.detailPanel.operation.remove')).toBeInTheDocument()
})
it('should render correctly for tool plugin delete', () => {
render(<DetailHeader detail={createPluginDetail()} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
expect(screen.getByTestId('remove-btn'))!.toBeInTheDocument()
openActionsMenu()
expect(screen.getByText('plugin.detailPanel.operation.remove')).toBeInTheDocument()
})
})
@ -772,20 +758,23 @@ describe('DetailHeader', () => {
})
render(<DetailHeader detail={detail} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
expect(screen.getByTestId('operation-dropdown'))!.toBeInTheDocument()
openActionsMenu()
expect(screen.getByRole('menuitem', { name: 'plugin.detailPanel.operation.viewDetail' })).toHaveAttribute('href', 'https://github.com/owner/repo')
})
it('should render marketplace source correctly', () => {
render(<DetailHeader detail={createPluginDetail()} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
expect(screen.getByTestId('operation-dropdown'))!.toBeInTheDocument()
openActionsMenu()
expect(screen.getByRole('menuitem', { name: 'plugin.detailPanel.operation.viewDetail' })).toHaveAttribute('href', 'https://marketplace.example.com/plugins/test-author/test-plugin-name')
})
it('should render local source correctly', () => {
const detail = createPluginDetail({ source: PluginSource.local })
render(<DetailHeader detail={detail} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
expect(screen.getByTestId('operation-dropdown'))!.toBeInTheDocument()
openActionsMenu()
expect(screen.queryByText('plugin.detailPanel.operation.viewDetail')).not.toBeInTheDocument()
})
})
@ -794,7 +783,7 @@ describe('DetailHeader', () => {
const detail = createPluginDetail()
render(<DetailHeader detail={detail} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
fireEvent.click(screen.getByTestId('view-readme-btn'))
clickOperation('plugin.detailPanel.operation.viewReadme')
expect(mockOpenReadmePanel).toHaveBeenCalledWith({
detail,
@ -805,13 +794,15 @@ describe('DetailHeader', () => {
it('should not expose README action for builtin tools', () => {
render(<DetailHeader detail={createPluginDetail({ id: 'code' })} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
expect(screen.queryByTestId('view-readme-btn')).not.toBeInTheDocument()
openActionsMenu()
expect(screen.queryByText('plugin.detailPanel.operation.viewReadme')).not.toBeInTheDocument()
})
it('should not expose README action when plugin unique identifier is missing', () => {
render(<DetailHeader detail={createPluginDetail({ plugin_unique_identifier: '' })} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
expect(screen.queryByTestId('view-readme-btn')).not.toBeInTheDocument()
openActionsMenu()
expect(screen.queryByText('plugin.detailPanel.operation.viewReadme')).not.toBeInTheDocument()
})
})
@ -878,7 +869,7 @@ describe('DetailHeader', () => {
it('should show delete confirm when remove button is clicked', async () => {
render(<DetailHeader detail={createPluginDetail()} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
fireEvent.click(screen.getByTestId('remove-btn'))
clickOperation('plugin.detailPanel.operation.remove')
await waitFor(() => {
expect(screen.getByRole('alertdialog'))!.toBeInTheDocument()
@ -888,7 +879,7 @@ describe('DetailHeader', () => {
it('should hide delete confirm when cancel is clicked', async () => {
render(<DetailHeader detail={createPluginDetail()} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
fireEvent.click(screen.getByTestId('remove-btn'))
clickOperation('plugin.detailPanel.operation.remove')
await waitFor(() => {
expect(screen.getByRole('alertdialog'))!.toBeInTheDocument()
})
@ -903,7 +894,7 @@ describe('DetailHeader', () => {
it('should call uninstallPlugin when confirm delete is clicked', async () => {
render(<DetailHeader detail={createPluginDetail()} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
fireEvent.click(screen.getByTestId('remove-btn'))
clickOperation('plugin.detailPanel.operation.remove')
await waitFor(() => {
expect(screen.getByRole('alertdialog'))!.toBeInTheDocument()
})
@ -918,7 +909,7 @@ describe('DetailHeader', () => {
it('should call onUpdate with true after successful delete', async () => {
render(<DetailHeader detail={createPluginDetail()} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
fireEvent.click(screen.getByTestId('remove-btn'))
clickOperation('plugin.detailPanel.operation.remove')
await waitFor(() => {
expect(screen.getByRole('alertdialog'))!.toBeInTheDocument()
})
@ -939,7 +930,7 @@ describe('DetailHeader', () => {
})
render(<DetailHeader detail={detail} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
fireEvent.click(screen.getByTestId('remove-btn'))
clickOperation('plugin.detailPanel.operation.remove')
await waitFor(() => {
expect(screen.getByRole('alertdialog'))!.toBeInTheDocument()
})
@ -954,7 +945,7 @@ describe('DetailHeader', () => {
it('should invalidate tool providers when deleting tool plugin', async () => {
render(<DetailHeader detail={createPluginDetail()} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
fireEvent.click(screen.getByTestId('remove-btn'))
clickOperation('plugin.detailPanel.operation.remove')
await waitFor(() => {
expect(screen.getByRole('alertdialog'))!.toBeInTheDocument()
})
@ -969,7 +960,7 @@ describe('DetailHeader', () => {
it('should track plugin uninstalled event after successful delete', async () => {
render(<DetailHeader detail={createPluginDetail()} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
fireEvent.click(screen.getByTestId('remove-btn'))
clickOperation('plugin.detailPanel.operation.remove')
await waitFor(() => {
expect(screen.getByRole('alertdialog'))!.toBeInTheDocument()
})
@ -1038,9 +1029,13 @@ describe('DetailHeader', () => {
describe('Plugin Info Modal', () => {
it('should show plugin info modal when info button is clicked', async () => {
render(<DetailHeader detail={createPluginDetail()} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
const detail = createPluginDetail({
source: PluginSource.github,
meta: { repo: 'owner/repo', version: 'v1.0.0', package: 'pkg' },
})
render(<DetailHeader detail={detail} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
fireEvent.click(screen.getByTestId('info-btn'))
clickOperation('plugin.detailPanel.operation.info')
await waitFor(() => {
expect(screen.getByTestId('plugin-info'))!.toBeInTheDocument()
@ -1048,9 +1043,13 @@ describe('DetailHeader', () => {
})
it('should hide plugin info modal when close button is clicked', async () => {
render(<DetailHeader detail={createPluginDetail()} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
const detail = createPluginDetail({
source: PluginSource.github,
meta: { repo: 'owner/repo', version: 'v1.0.0', package: 'pkg' },
})
render(<DetailHeader detail={detail} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
fireEvent.click(screen.getByTestId('info-btn'))
clickOperation('plugin.detailPanel.operation.info')
await waitFor(() => {
expect(screen.getByTestId('plugin-info'))!.toBeInTheDocument()
})
@ -1069,7 +1068,8 @@ describe('DetailHeader', () => {
})
render(<DetailHeader detail={detail} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
expect(screen.getByTestId('info-btn'))!.toBeInTheDocument()
openActionsMenu()
expect(screen.getByText('plugin.detailPanel.operation.info')).toBeInTheDocument()
})
})
})

View File

@ -1,35 +1,16 @@
import type { ReactElement, ReactNode } from 'react'
import type { ReactElement } from 'react'
import { fireEvent, screen } from '@testing-library/react'
import { cloneElement } from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
import { PluginSource } from '../../types'
import OperationDropdown from '../operation-dropdown'
import { OperationDropdown } from '../operation-dropdown'
const render = (ui: ReactElement) =>
renderWithSystemFeatures(ui, { systemFeatures: { enable_marketplace: true } })
const render = (ui: ReactElement, enableMarketplace = true) =>
renderWithSystemFeatures(ui, { systemFeatures: { enable_marketplace: enableMarketplace } })
vi.mock('@langgenius/dify-ui/cn', () => ({
cn: (...args: (string | undefined | false | null)[]) => args.filter(Boolean).join(' '),
}))
vi.mock('@langgenius/dify-ui/dropdown-menu', () => ({
DropdownMenu: ({ children, open }: { children: ReactNode, open: boolean }) => (
<div data-testid="dropdown-menu" data-open={open}>{children}</div>
),
DropdownMenuTrigger: ({ children, className }: { children: ReactNode, className?: string }) => (
<button data-testid="dropdown-trigger" className={className}>{children}</button>
),
DropdownMenuContent: ({ children, popupClassName }: { children: ReactNode, popupClassName?: string }) => (
<div data-testid="dropdown-content" className={popupClassName}>{children}</div>
),
DropdownMenuItem: ({ children, className, onClick, render, variant }: { children: ReactNode, className?: string, onClick?: () => void, render?: ReactElement, variant?: string }) => {
if (render)
return cloneElement(render, { className, onClick, 'data-variant': variant } as Record<string, unknown>, children)
return <div data-testid="dropdown-item" className={className} data-variant={variant} onClick={onClick}>{children}</div>
},
DropdownMenuSeparator: ({ className }: { className?: string }) => <hr data-testid="dropdown-separator" className={className} />,
}))
const openDropdown = () => {
fireEvent.click(screen.getByRole('button', { name: 'plugin.detailPanel.operation.moreActions' }))
}
describe('OperationDropdown', () => {
const mockOnInfo = vi.fn()
@ -49,206 +30,126 @@ describe('OperationDropdown', () => {
})
describe('Rendering', () => {
it('should render trigger button', () => {
it('should render the actions trigger', () => {
render(<OperationDropdown {...defaultProps} />)
expect(screen.getByTestId('dropdown-trigger')).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'plugin.detailPanel.operation.moreActions' })).toBeInTheDocument()
})
it('should render dropdown content', () => {
render(<OperationDropdown {...defaultProps} />)
expect(screen.getByTestId('dropdown-content')).toBeInTheDocument()
})
it('should render figma-aligned dropdown surface and rows', () => {
render(<OperationDropdown {...defaultProps} />)
expect(screen.getByTestId('dropdown-content')).toHaveClass('w-[192px]', 'py-1')
expect(screen.getByText('plugin.detailPanel.operation.viewDetail').closest('a')).toHaveClass('px-2', 'py-1', 'system-md-regular')
expect(screen.getByText('plugin.detailPanel.operation.remove').closest('[data-testid="dropdown-item"]')).toHaveClass('px-2', 'py-1', 'system-md-regular')
expect(screen.getByTestId('dropdown-separator')).toHaveClass('my-0')
})
it('should render info option for github source', () => {
it('should render GitHub actions when marketplace is enabled', () => {
render(<OperationDropdown {...defaultProps} source={PluginSource.github} />)
openDropdown()
expect(screen.getByText('plugin.detailPanel.operation.info')).toBeInTheDocument()
})
it('should render check update option for github source', () => {
render(<OperationDropdown {...defaultProps} source={PluginSource.github} />)
expect(screen.getByText('plugin.detailPanel.operation.checkUpdate')).toBeInTheDocument()
})
it('should render view detail option for github source with marketplace enabled', () => {
render(<OperationDropdown {...defaultProps} source={PluginSource.github} />)
expect(screen.getByText('plugin.detailPanel.operation.viewDetail')).toBeInTheDocument()
})
it('should render view detail option for marketplace source', () => {
render(<OperationDropdown {...defaultProps} source={PluginSource.marketplace} />)
expect(screen.getByText('plugin.detailPanel.operation.viewDetail')).toBeInTheDocument()
})
it('should render view README option when provided', () => {
render(<OperationDropdown {...defaultProps} onViewReadme={mockOnViewReadme} />)
expect(screen.getByText('plugin.detailPanel.operation.viewReadme')).toBeInTheDocument()
})
it('should render view README below view marketplace', () => {
render(<OperationDropdown {...defaultProps} onViewReadme={mockOnViewReadme} source={PluginSource.marketplace} />)
const viewMarketplace = screen.getByText('plugin.detailPanel.operation.viewDetail').closest('a')!
const viewReadme = screen.getByText('plugin.detailPanel.operation.viewReadme').closest('[data-testid="dropdown-item"]')!
expect(viewMarketplace.compareDocumentPosition(viewReadme)).toBe(Node.DOCUMENT_POSITION_FOLLOWING)
})
it('should not render view README option when it is unavailable', () => {
render(<OperationDropdown {...defaultProps} />)
expect(screen.queryByText('plugin.detailPanel.operation.viewReadme')).not.toBeInTheDocument()
})
it('should always render remove option', () => {
render(<OperationDropdown {...defaultProps} />)
expect(screen.getByText('plugin.detailPanel.operation.remove')).toBeInTheDocument()
})
it('should render remove option with normal text color', () => {
render(<OperationDropdown {...defaultProps} />)
expect(screen.getByText('plugin.detailPanel.operation.remove').closest('[data-testid="dropdown-item"]')).not.toHaveAttribute('data-variant', 'destructive')
})
it('should render destructive hover styles for remove option when requested', () => {
render(<OperationDropdown {...defaultProps} destructiveRemove />)
expect(screen.getByText('plugin.detailPanel.operation.remove').closest('[data-testid="dropdown-item"]')).toHaveClass(
'data-highlighted:bg-state-destructive-hover',
'data-highlighted:text-text-destructive',
)
})
it('should not render info option for marketplace source', () => {
it('should render marketplace detail action for marketplace source', () => {
render(<OperationDropdown {...defaultProps} source={PluginSource.marketplace} />)
openDropdown()
expect(screen.queryByText('plugin.detailPanel.operation.info')).not.toBeInTheDocument()
})
it('should not render check update option for marketplace source', () => {
render(<OperationDropdown {...defaultProps} source={PluginSource.marketplace} />)
expect(screen.queryByText('plugin.detailPanel.operation.checkUpdate')).not.toBeInTheDocument()
expect(screen.getByText('plugin.detailPanel.operation.viewDetail')).toBeInTheDocument()
expect(screen.getByText('plugin.detailPanel.operation.remove')).toBeInTheDocument()
})
it('should not render view detail for local source', () => {
it('should render README action only when provided', () => {
const { unmount } = render(<OperationDropdown {...defaultProps} />)
openDropdown()
expect(screen.queryByText('plugin.detailPanel.operation.viewReadme')).not.toBeInTheDocument()
unmount()
render(<OperationDropdown {...defaultProps} onViewReadme={mockOnViewReadme} />)
openDropdown()
expect(screen.getByText('plugin.detailPanel.operation.viewReadme')).toBeInTheDocument()
})
it('should not render marketplace detail when source cannot open a detail page', () => {
render(<OperationDropdown {...defaultProps} source={PluginSource.local} />)
openDropdown()
expect(screen.queryByText('plugin.detailPanel.operation.viewDetail')).not.toBeInTheDocument()
expect(screen.getByText('plugin.detailPanel.operation.remove')).toBeInTheDocument()
})
it('should not render view detail for debugging source', () => {
render(<OperationDropdown {...defaultProps} source={PluginSource.debugging} />)
it('should not render the trigger when every action is unavailable', () => {
render(
<OperationDropdown
{...defaultProps}
source={PluginSource.local}
showRemove={false}
showCheckVersion={false}
/>,
)
expect(screen.queryByText('plugin.detailPanel.operation.viewDetail')).not.toBeInTheDocument()
expect(screen.queryByRole('button', { name: 'plugin.detailPanel.operation.moreActions' })).not.toBeInTheDocument()
})
})
describe('User Interactions', () => {
it('should render dropdown menu root', () => {
render(<OperationDropdown {...defaultProps} />)
expect(screen.getByTestId('dropdown-menu')).toBeInTheDocument()
})
it('should call onInfo when info option is clicked', () => {
it('should call onInfo when the info action is clicked', () => {
render(<OperationDropdown {...defaultProps} source={PluginSource.github} />)
openDropdown()
fireEvent.click(screen.getByText('plugin.detailPanel.operation.info'))
expect(mockOnInfo).toHaveBeenCalledTimes(1)
})
it('should call onCheckVersion when check update option is clicked', () => {
it('should call onCheckVersion when the check update action is clicked', () => {
render(<OperationDropdown {...defaultProps} source={PluginSource.github} />)
openDropdown()
fireEvent.click(screen.getByText('plugin.detailPanel.operation.checkUpdate'))
expect(mockOnCheckVersion).toHaveBeenCalledTimes(1)
})
it('should call onRemove when remove option is clicked', () => {
render(<OperationDropdown {...defaultProps} />)
it('should call onRemove when the remove action is clicked', () => {
render(<OperationDropdown {...defaultProps} source={PluginSource.github} />)
openDropdown()
fireEvent.click(screen.getByText('plugin.detailPanel.operation.remove'))
expect(mockOnRemove).toHaveBeenCalledTimes(1)
})
it('should call onViewReadme when view README option is clicked', () => {
it('should call onViewReadme when README action is clicked', () => {
render(<OperationDropdown {...defaultProps} onViewReadme={mockOnViewReadme} />)
openDropdown()
fireEvent.click(screen.getByText('plugin.detailPanel.operation.viewReadme'))
expect(mockOnViewReadme).toHaveBeenCalledTimes(1)
})
it('should have correct href for view detail link', () => {
it('should render view detail as an external link', () => {
render(<OperationDropdown {...defaultProps} source={PluginSource.github} />)
openDropdown()
const link = screen.getByText('plugin.detailPanel.operation.viewDetail').closest('a')
expect(link).toHaveAttribute('href', 'https://github.com/test/repo')
expect(link).toHaveAttribute('target', '_blank')
expect(link).toHaveAttribute('rel', 'noopener noreferrer')
})
})
describe('Props Variations', () => {
it('should handle all plugin sources', () => {
const sources = [
PluginSource.github,
PluginSource.marketplace,
PluginSource.local,
PluginSource.debugging,
]
describe('Feature Flags', () => {
it('should hide marketplace detail when marketplace is disabled', () => {
render(<OperationDropdown {...defaultProps} source={PluginSource.github} />, false)
sources.forEach((source) => {
const { unmount } = render(
<OperationDropdown {...defaultProps} source={source} />,
)
expect(screen.getByTestId('dropdown-menu')).toBeInTheDocument()
expect(screen.getByText('plugin.detailPanel.operation.remove')).toBeInTheDocument()
unmount()
})
})
openDropdown()
it('should handle different detail URLs', () => {
const urls = [
'https://github.com/owner/repo',
'https://marketplace.example.com/plugin/123',
]
urls.forEach((url) => {
const { unmount } = render(
<OperationDropdown {...defaultProps} detailUrl={url} source={PluginSource.github} />,
)
const link = screen.getByText('plugin.detailPanel.operation.viewDetail').closest('a')
expect(link).toHaveAttribute('href', url)
unmount()
})
})
})
describe('Memoization', () => {
it('should be wrapped with React.memo', () => {
expect(OperationDropdown).toBeDefined()
expect((OperationDropdown as { $$typeof?: symbol }).$$typeof).toBeDefined()
expect(screen.queryByText('plugin.detailPanel.operation.viewDetail')).not.toBeInTheDocument()
expect(screen.getByText('plugin.detailPanel.operation.info')).toBeInTheDocument()
})
})
})

View File

@ -1,9 +1,10 @@
import type { ReactElement } from 'react'
import type { PluginDetail } from '@/app/components/plugins/types'
import { fireEvent, render as rtlRender, screen } from '@testing-library/react'
import { fireEvent, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
import { PluginCategoryEnum, PluginSource } from '@/app/components/plugins/types'
import { createAccountProfileQueryWrapper } from '@/test/account-profile-query'
import { createAccountProfileQueryClient } from '@/test/account-profile-query'
import DetailHeader from '../index'
const mockSetTargetVersion = vi.fn()
@ -13,8 +14,11 @@ const mockHandleUpdatedFromMarketplace = vi.fn()
const mockHandleDelete = vi.fn()
const render = (ui: ReactElement) => {
const Wrapper = createAccountProfileQueryWrapper({ timezone: 'UTC' })
return rtlRender(ui, { wrapper: Wrapper })
const queryClient = createAccountProfileQueryClient({ timezone: 'UTC' })
return renderWithSystemFeatures(ui, {
queryClient,
systemFeatures: { enable_marketplace: true },
})
}
vi.mock('@/context/app-context', () => ({
@ -79,10 +83,6 @@ vi.mock('@/app/components/plugins/plugin-auth', () => ({
),
}))
vi.mock('@/app/components/plugins/plugin-detail-panel/operation-dropdown', () => ({
default: ({ detailUrl }: { detailUrl: string }) => <div data-testid="operation-dropdown">{detailUrl}</div>,
}))
vi.mock('@/app/components/plugins/update-plugin/plugin-version-picker', () => ({
default: ({ onSelect, trigger }: {
onSelect: (value: { version: string, unique_identifier: string, isDowngrade?: boolean }) => void
@ -235,7 +235,8 @@ describe('DetailHeader', () => {
expect(screen.getByTestId('description')).toHaveTextContent('Tool plugin description')
expect(screen.getByTestId('source-badge')).toHaveTextContent('marketplace')
expect(screen.getByTestId('plugin-auth')).toHaveTextContent('tool-plugin/provider-a')
expect(screen.getByTestId('operation-dropdown')).toHaveTextContent('https://marketplace.example.com/plugins/acme/provider-a')
fireEvent.click(screen.getByRole('button', { name: 'plugin.detailPanel.operation.moreActions' }))
expect(screen.getByRole('menuitem', { name: 'plugin.detailPanel.operation.viewDetail' })).toHaveAttribute('href', 'https://marketplace.example.com/plugins/acme/provider-a')
expect(screen.getByTestId('header-modals')).toBeInTheDocument()
})

View File

@ -10,7 +10,7 @@ import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import Badge from '@/app/components/base/badge'
import { AuthCategory, PluginAuth } from '@/app/components/plugins/plugin-auth'
import OperationDropdown from '@/app/components/plugins/plugin-detail-panel/operation-dropdown'
import { OperationDropdown } from '@/app/components/plugins/plugin-detail-panel/operation-dropdown'
import { BUILTIN_TOOLS_ARRAY } from '@/app/components/plugins/readme-panel/constants'
import { useReadmePanelStore } from '@/app/components/plugins/readme-panel/store'
import PluginVersionPicker from '@/app/components/plugins/update-plugin/plugin-version-picker'

View File

@ -1,21 +1,20 @@
'use client'
import type { Placement } from '@langgenius/dify-ui/dropdown-menu'
import type { FC } from 'react'
import { cn } from '@langgenius/dify-ui/cn'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLinkItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@langgenius/dify-ui/dropdown-menu'
import { useSuspenseQuery } from '@tanstack/react-query'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
import { PluginSource } from '../types'
type Props = Readonly<{
type OperationDropdownProps = Readonly<{
source: PluginSource
onInfo: () => void
onCheckVersion: () => void
@ -32,11 +31,7 @@ type Props = Readonly<{
showRemove?: boolean
}>
const operationMenuPopupClassName = 'w-[192px] py-1'
const operationMenuItemClassName = 'px-2 py-1 text-text-secondary system-md-regular'
const operationMenuLabelClassName = 'min-w-0 grow truncate px-1 py-0.5'
const OperationDropdown: FC<Props> = ({
export function OperationDropdown({
source,
detailUrl,
onInfo,
@ -51,7 +46,7 @@ const OperationDropdown: FC<Props> = ({
destructiveRemove = false,
showCheckVersion = true,
showRemove = true,
}) => {
}: OperationDropdownProps) {
const { t } = useTranslation()
const { data: enable_marketplace } = useSuspenseQuery({
...systemFeaturesQueryOptions(),
@ -70,58 +65,55 @@ const OperationDropdown: FC<Props> = ({
<DropdownMenu>
<DropdownMenuTrigger
className={cn('action-btn data-popup-open:bg-state-base-hover', triggerSize === 'xs' ? 'action-btn-xs' : 'action-btn-m')}
aria-label={t('detailPanel.operation.moreActions', { ns: 'plugin' })}
>
<span className="i-ri-more-fill size-4" />
<span aria-hidden className="i-ri-more-fill size-4" />
</DropdownMenuTrigger>
<DropdownMenuContent
placement={placement}
sideOffset={sideOffset}
alignOffset={alignOffset}
popupClassName={cn(operationMenuPopupClassName, popupClassName)}
popupClassName={cn('w-[192px] py-1', popupClassName)}
>
{showInfo && (
<DropdownMenuItem className={operationMenuItemClassName} onClick={onInfo}>
<span className={operationMenuLabelClassName}>{t('detailPanel.operation.info', { ns: 'plugin' })}</span>
<DropdownMenuItem className="px-2 py-1 system-md-regular text-text-secondary" onClick={onInfo}>
<span className="min-w-0 grow truncate px-1 py-0.5">{t('detailPanel.operation.info', { ns: 'plugin' })}</span>
</DropdownMenuItem>
)}
{showCheckVersionAction && (
<DropdownMenuItem className={operationMenuItemClassName} onClick={onCheckVersion}>
<span className={operationMenuLabelClassName}>{t('detailPanel.operation.checkUpdate', { ns: 'plugin' })}</span>
<DropdownMenuItem className="px-2 py-1 system-md-regular text-text-secondary" onClick={onCheckVersion}>
<span className="min-w-0 grow truncate px-1 py-0.5">{t('detailPanel.operation.checkUpdate', { ns: 'plugin' })}</span>
</DropdownMenuItem>
)}
{showMarketplaceDetail && (
<DropdownMenuItem
className={operationMenuItemClassName}
render={(
<a
href={detailUrl}
target="_blank"
rel="noopener noreferrer"
aria-label={t('detailPanel.operation.viewDetail', { ns: 'plugin' })}
/>
)}
<DropdownMenuLinkItem
className="px-2 py-1 system-md-regular text-text-secondary"
href={detailUrl}
target="_blank"
rel="noopener noreferrer"
aria-label={t('detailPanel.operation.viewDetail', { ns: 'plugin' })}
>
<span className={operationMenuLabelClassName}>{t('detailPanel.operation.viewDetail', { ns: 'plugin' })}</span>
<span className="min-w-0 grow truncate px-1 py-0.5">{t('detailPanel.operation.viewDetail', { ns: 'plugin' })}</span>
<span className="i-ri-arrow-right-up-line size-3.5 shrink-0 text-text-tertiary" />
</DropdownMenuItem>
</DropdownMenuLinkItem>
)}
{onViewReadme && (
<DropdownMenuItem className={operationMenuItemClassName} onClick={onViewReadme}>
<span className={operationMenuLabelClassName}>{t('detailPanel.operation.viewReadme', { ns: 'plugin' })}</span>
<DropdownMenuItem className="px-2 py-1 system-md-regular text-text-secondary" onClick={onViewReadme}>
<span className="min-w-0 grow truncate px-1 py-0.5">{t('detailPanel.operation.viewReadme', { ns: 'plugin' })}</span>
</DropdownMenuItem>
)}
{showSeparator && (
<DropdownMenuSeparator className="my-0" />
<DropdownMenuSeparator />
)}
{showRemoveAction && (
<DropdownMenuItem
className={cn(
operationMenuItemClassName,
'px-2 py-1 system-md-regular text-text-secondary',
destructiveRemove && 'data-highlighted:bg-state-destructive-hover data-highlighted:text-text-destructive',
)}
onClick={onRemove}
>
<span className={cn(operationMenuLabelClassName, destructiveRemove && 'text-inherit')}>
<span className={cn('min-w-0 grow truncate px-1 py-0.5', destructiveRemove && 'text-inherit')}>
{t('detailPanel.operation.remove', { ns: 'plugin' })}
</span>
</DropdownMenuItem>
@ -130,4 +122,3 @@ const OperationDropdown: FC<Props> = ({
</DropdownMenu>
)
}
export default React.memo(OperationDropdown)

View File

@ -131,6 +131,7 @@
"detailPanel.operation.detail": "التفاصيل",
"detailPanel.operation.info": "معلومات الإضافة",
"detailPanel.operation.install": "تثبيت",
"detailPanel.operation.moreActions": "المزيد من الإجراءات",
"detailPanel.operation.remove": "إزالة",
"detailPanel.operation.update": "تحديث",
"detailPanel.operation.updateTooltip": "قم بالتحديث للوصول إلى أحدث النماذج.",

View File

@ -131,6 +131,7 @@
"detailPanel.operation.detail": "Einzelheiten",
"detailPanel.operation.info": "Plugin-Informationen",
"detailPanel.operation.install": "Installieren",
"detailPanel.operation.moreActions": "Weitere Aktionen",
"detailPanel.operation.remove": "Entfernen",
"detailPanel.operation.update": "Aktualisieren",
"detailPanel.operation.updateTooltip": "Aktualisieren Sie, um auf die neuesten Modelle zuzugreifen.",

View File

@ -131,6 +131,7 @@
"detailPanel.operation.detail": "Details",
"detailPanel.operation.info": "Integration Info",
"detailPanel.operation.install": "Install",
"detailPanel.operation.moreActions": "More actions",
"detailPanel.operation.remove": "Remove",
"detailPanel.operation.update": "Update",
"detailPanel.operation.updateTooltip": "Update to access the latest models.",

View File

@ -131,6 +131,7 @@
"detailPanel.operation.detail": "Detalles",
"detailPanel.operation.info": "Información del plugin",
"detailPanel.operation.install": "Instalar",
"detailPanel.operation.moreActions": "Más acciones",
"detailPanel.operation.remove": "Eliminar",
"detailPanel.operation.update": "Actualizar",
"detailPanel.operation.updateTooltip": "Actualiza para acceder a los modelos más recientes.",

View File

@ -131,6 +131,7 @@
"detailPanel.operation.detail": "جزئیات",
"detailPanel.operation.info": "اطلاعات پلاگین",
"detailPanel.operation.install": "نصب",
"detailPanel.operation.moreActions": "اقدامات بیشتر",
"detailPanel.operation.remove": "حذف",
"detailPanel.operation.update": "روز رسانی",
"detailPanel.operation.updateTooltip": "به‌روزرسانی کنید تا به جدیدترین مدل‌ها دسترسی پیدا کنید.",

View File

@ -131,6 +131,7 @@
"detailPanel.operation.detail": "Détails",
"detailPanel.operation.info": "Informations sur le plugin",
"detailPanel.operation.install": "Installer",
"detailPanel.operation.moreActions": "Plus dactions",
"detailPanel.operation.remove": "Enlever",
"detailPanel.operation.update": "Mettre à jour",
"detailPanel.operation.updateTooltip": "Mettez à jour pour accéder aux derniers modèles.",

View File

@ -131,6 +131,7 @@
"detailPanel.operation.detail": "विवरण",
"detailPanel.operation.info": "प्लगइन जानकारी",
"detailPanel.operation.install": "स्थापित करें",
"detailPanel.operation.moreActions": "अधिक कार्रवाइयां",
"detailPanel.operation.remove": "हटाएं",
"detailPanel.operation.update": "अपडेट",
"detailPanel.operation.updateTooltip": "नवीनतम मॉडल तक पहुँचने के लिए अपडेट करें।",

View File

@ -131,6 +131,7 @@
"detailPanel.operation.detail": "Rincian",
"detailPanel.operation.info": "Plugin Info",
"detailPanel.operation.install": "Pasang",
"detailPanel.operation.moreActions": "Tindakan lainnya",
"detailPanel.operation.remove": "Hapus",
"detailPanel.operation.update": "Pemutakhiran",
"detailPanel.operation.updateTooltip": "Perbarui untuk mengakses model terbaru.",

View File

@ -131,6 +131,7 @@
"detailPanel.operation.detail": "Dettagli",
"detailPanel.operation.info": "Informazioni sul plugin",
"detailPanel.operation.install": "Installare",
"detailPanel.operation.moreActions": "Altre azioni",
"detailPanel.operation.remove": "Togliere",
"detailPanel.operation.update": "Aggiornare",
"detailPanel.operation.updateTooltip": "Aggiorna per accedere ai modelli più recenti.",

View File

@ -131,6 +131,7 @@
"detailPanel.operation.detail": "詳細",
"detailPanel.operation.info": "プラグイン情報",
"detailPanel.operation.install": "インストール",
"detailPanel.operation.moreActions": "その他の操作",
"detailPanel.operation.remove": "削除",
"detailPanel.operation.update": "更新",
"detailPanel.operation.updateTooltip": "最新のモデルにアクセスするために更新してください。",

View File

@ -131,6 +131,7 @@
"detailPanel.operation.detail": "세부 정보",
"detailPanel.operation.info": "플러그인 정보",
"detailPanel.operation.install": "설치",
"detailPanel.operation.moreActions": "추가 작업",
"detailPanel.operation.remove": "제거",
"detailPanel.operation.update": "업데이트",
"detailPanel.operation.updateTooltip": "최신 모델에 액세스하려면 업데이트하세요.",

View File

@ -131,6 +131,7 @@
"detailPanel.operation.detail": "Details",
"detailPanel.operation.info": "Plugin Info",
"detailPanel.operation.install": "Install",
"detailPanel.operation.moreActions": "Meer acties",
"detailPanel.operation.remove": "Remove",
"detailPanel.operation.update": "Update",
"detailPanel.operation.updateTooltip": "Werk bij voor toegang tot de nieuwste modellen.",

View File

@ -131,6 +131,7 @@
"detailPanel.operation.detail": "Szczegóły",
"detailPanel.operation.info": "Informacje o wtyczce",
"detailPanel.operation.install": "Instalować",
"detailPanel.operation.moreActions": "Więcej działań",
"detailPanel.operation.remove": "Usunąć",
"detailPanel.operation.update": "Aktualizacja",
"detailPanel.operation.updateTooltip": "Zaktualizuj, aby uzyskać dostęp do najnowszych modeli.",

View File

@ -131,6 +131,7 @@
"detailPanel.operation.detail": "Detalhes",
"detailPanel.operation.info": "Informações do plugin",
"detailPanel.operation.install": "Instalar",
"detailPanel.operation.moreActions": "Mais ações",
"detailPanel.operation.remove": "Retirar",
"detailPanel.operation.update": "Atualização",
"detailPanel.operation.updateTooltip": "Atualize para acessar os modelos mais recentes.",

View File

@ -131,6 +131,7 @@
"detailPanel.operation.detail": "Detalii",
"detailPanel.operation.info": "Informații despre plugin",
"detailPanel.operation.install": "Instala",
"detailPanel.operation.moreActions": "Mai multe acțiuni",
"detailPanel.operation.remove": "Depărta",
"detailPanel.operation.update": "Actualiza",
"detailPanel.operation.updateTooltip": "Actualizați pentru a accesa cele mai recente modele.",

View File

@ -131,6 +131,7 @@
"detailPanel.operation.detail": "Подробности",
"detailPanel.operation.info": "Информация о плагине",
"detailPanel.operation.install": "Устанавливать",
"detailPanel.operation.moreActions": "Другие действия",
"detailPanel.operation.remove": "Убирать",
"detailPanel.operation.update": "Обновлять",
"detailPanel.operation.updateTooltip": "Обновите для доступа к последним моделям.",

View File

@ -131,6 +131,7 @@
"detailPanel.operation.detail": "Podrobnosti",
"detailPanel.operation.info": "Informacije o vtičniku",
"detailPanel.operation.install": "Namestite",
"detailPanel.operation.moreActions": "Več dejanj",
"detailPanel.operation.remove": "Odstrani",
"detailPanel.operation.update": "Posodobitev",
"detailPanel.operation.updateTooltip": "Posodobite za dostop do najnovejših modelov.",

View File

@ -131,6 +131,7 @@
"detailPanel.operation.detail": "ราย ละเอียด",
"detailPanel.operation.info": "ข้อมูลปลั๊กอิน",
"detailPanel.operation.install": "ติดตั้ง",
"detailPanel.operation.moreActions": "การดำเนินการเพิ่มเติม",
"detailPanel.operation.remove": "ถอด",
"detailPanel.operation.update": "อัพเดต",
"detailPanel.operation.updateTooltip": "อัปเดตเพื่อเข้าถึงโมเดลล่าสุด",

View File

@ -131,6 +131,7 @@
"detailPanel.operation.detail": "Detay",
"detailPanel.operation.info": "Eklenti Bilgileri",
"detailPanel.operation.install": "Yükle",
"detailPanel.operation.moreActions": "Diğer işlemler",
"detailPanel.operation.remove": "Kaldır",
"detailPanel.operation.update": "Güncelle",
"detailPanel.operation.updateTooltip": "En son modellere erişmek için güncelleyin.",

View File

@ -131,6 +131,7 @@
"detailPanel.operation.detail": "Деталі",
"detailPanel.operation.info": "Інформація про плагін",
"detailPanel.operation.install": "Інсталювати",
"detailPanel.operation.moreActions": "Більше дій",
"detailPanel.operation.remove": "Видалити",
"detailPanel.operation.update": "Оновлювати",
"detailPanel.operation.updateTooltip": "Оновіть, щоб отримати доступ до найновіших моделей.",

View File

@ -131,6 +131,7 @@
"detailPanel.operation.detail": "Chi tiết",
"detailPanel.operation.info": "Thông tin plugin",
"detailPanel.operation.install": "Cài đặt",
"detailPanel.operation.moreActions": "Thao tác khác",
"detailPanel.operation.remove": "Triệt",
"detailPanel.operation.update": "Cập nhật",
"detailPanel.operation.updateTooltip": "Cập nhật để truy cập các mô hình mới nhất.",

View File

@ -131,6 +131,7 @@
"detailPanel.operation.detail": "详情",
"detailPanel.operation.info": "集成信息",
"detailPanel.operation.install": "安装",
"detailPanel.operation.moreActions": "更多操作",
"detailPanel.operation.remove": "移除",
"detailPanel.operation.update": "更新",
"detailPanel.operation.updateTooltip": "更新以获取最新模型。",

View File

@ -131,6 +131,7 @@
"detailPanel.operation.detail": "詳",
"detailPanel.operation.info": "插件資訊",
"detailPanel.operation.install": "安裝",
"detailPanel.operation.moreActions": "更多操作",
"detailPanel.operation.remove": "刪除",
"detailPanel.operation.update": "更新",
"detailPanel.operation.updateTooltip": "更新以取得最新模型。",