mirror of
https://github.com/langgenius/dify.git
synced 2026-07-21 18:58:35 +08:00
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:
parent
1b3e8e9943
commit
750075c859
@ -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
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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()
|
||||
})
|
||||
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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": "قم بالتحديث للوصول إلى أحدث النماذج.",
|
||||
|
||||
@ -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.",
|
||||
|
||||
@ -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.",
|
||||
|
||||
@ -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.",
|
||||
|
||||
@ -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": "بهروزرسانی کنید تا به جدیدترین مدلها دسترسی پیدا کنید.",
|
||||
|
||||
@ -131,6 +131,7 @@
|
||||
"detailPanel.operation.detail": "Détails",
|
||||
"detailPanel.operation.info": "Informations sur le plugin",
|
||||
"detailPanel.operation.install": "Installer",
|
||||
"detailPanel.operation.moreActions": "Plus d’actions",
|
||||
"detailPanel.operation.remove": "Enlever",
|
||||
"detailPanel.operation.update": "Mettre à jour",
|
||||
"detailPanel.operation.updateTooltip": "Mettez à jour pour accéder aux derniers modèles.",
|
||||
|
||||
@ -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": "नवीनतम मॉडल तक पहुँचने के लिए अपडेट करें।",
|
||||
|
||||
@ -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.",
|
||||
|
||||
@ -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.",
|
||||
|
||||
@ -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": "最新のモデルにアクセスするために更新してください。",
|
||||
|
||||
@ -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": "최신 모델에 액세스하려면 업데이트하세요.",
|
||||
|
||||
@ -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.",
|
||||
|
||||
@ -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.",
|
||||
|
||||
@ -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.",
|
||||
|
||||
@ -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.",
|
||||
|
||||
@ -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": "Обновите для доступа к последним моделям.",
|
||||
|
||||
@ -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.",
|
||||
|
||||
@ -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": "อัปเดตเพื่อเข้าถึงโมเดลล่าสุด",
|
||||
|
||||
@ -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.",
|
||||
|
||||
@ -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": "Оновіть, щоб отримати доступ до найновіших моделей.",
|
||||
|
||||
@ -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.",
|
||||
|
||||
@ -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": "更新以获取最新模型。",
|
||||
|
||||
@ -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": "更新以取得最新模型。",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user