From 750075c8590a8deb9c0472aa142edf4d5c5f94a8 Mon Sep 17 00:00:00 2001
From: yyh <92089059+lyzno1@users.noreply.github.com>
Date: Sun, 5 Jul 2026 22:00:40 +0800
Subject: [PATCH] style: fix provider card dropdown menu seperator margin
(#38422)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
---
eslint-suppressions.json | 8 -
.../__tests__/plugin-actions.spec.tsx | 17 +-
.../data-source-page-new/plugin-actions.tsx | 2 +-
.../__tests__/provider-card-actions.spec.tsx | 92 +++++---
.../provider-card-actions.tsx | 2 +-
.../__tests__/detail-header.spec.tsx | 126 +++++-----
.../__tests__/operation-dropdown.spec.tsx | 223 +++++-------------
.../detail-header/__tests__/index.spec.tsx | 19 +-
.../detail-header/index.tsx | 2 +-
.../operation-dropdown.tsx | 57 ++---
web/i18n/ar-TN/plugin.json | 1 +
web/i18n/de-DE/plugin.json | 1 +
web/i18n/en-US/plugin.json | 1 +
web/i18n/es-ES/plugin.json | 1 +
web/i18n/fa-IR/plugin.json | 1 +
web/i18n/fr-FR/plugin.json | 1 +
web/i18n/hi-IN/plugin.json | 1 +
web/i18n/id-ID/plugin.json | 1 +
web/i18n/it-IT/plugin.json | 1 +
web/i18n/ja-JP/plugin.json | 1 +
web/i18n/ko-KR/plugin.json | 1 +
web/i18n/nl-NL/plugin.json | 1 +
web/i18n/pl-PL/plugin.json | 1 +
web/i18n/pt-BR/plugin.json | 1 +
web/i18n/ro-RO/plugin.json | 1 +
web/i18n/ru-RU/plugin.json | 1 +
web/i18n/sl-SI/plugin.json | 1 +
web/i18n/th-TH/plugin.json | 1 +
web/i18n/tr-TR/plugin.json | 1 +
web/i18n/uk-UA/plugin.json | 1 +
web/i18n/vi-VN/plugin.json | 1 +
web/i18n/zh-Hans/plugin.json | 1 +
web/i18n/zh-Hant/plugin.json | 1 +
33 files changed, 245 insertions(+), 326 deletions(-)
diff --git a/eslint-suppressions.json b/eslint-suppressions.json
index d08aba6fad7..e4b92e2db43 100644
--- a/eslint-suppressions.json
+++ b/eslint-suppressions.json
@@ -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
diff --git a/web/app/components/header/account-setting/data-source-page-new/__tests__/plugin-actions.spec.tsx b/web/app/components/header/account-setting/data-source-page-new/__tests__/plugin-actions.spec.tsx
index f7bece10a3d..e0270a06b07 100644
--- a/web/app/components/header/account-setting/data-source-page-new/__tests__/plugin-actions.spec.tsx
+++ b/web/app/components/header/account-setting/data-source-page-new/__tests__/plugin-actions.spec.tsx
@@ -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 }) => (
-
- ),
-}))
-
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()
+ renderWithSystemFeatures(, {
+ 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({
diff --git a/web/app/components/header/account-setting/data-source-page-new/plugin-actions.tsx b/web/app/components/header/account-setting/data-source-page-new/plugin-actions.tsx
index 9cfd6bfaf1f..b4ffe0febaf 100644
--- a/web/app/components/header/account-setting/data-source-page-new/plugin-actions.tsx
+++ b/web/app/components/header/account-setting/data-source-page-new/plugin-actions.tsx
@@ -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'
diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/provider-card-actions.spec.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/provider-card-actions.spec.tsx
index 2b47e77d72f..b62b56a8a6f 100644
--- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/provider-card-actions.spec.tsx
+++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/provider-card-actions.spec.tsx
@@ -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
- }) => (
-
-
-
-
-
- ),
-}))
-
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()
- 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()
-
- 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(
+ ,
+ )
+
+ 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(
{
/>,
)
- expect(screen.getByTestId('operation-dropdown')).toHaveAttribute('data-detail-url', '')
+ openActionsMenu()
+ expect(screen.getByRole('menuitem', { name: 'plugin.detailPanel.operation.viewDetail' })).toHaveAttribute('href', '')
rerender(
{
/>,
)
- expect(screen.getByTestId('operation-dropdown')).toHaveAttribute('data-detail-url', '')
+ openActionsMenu()
+ expect(screen.queryByText('plugin.detailPanel.operation.viewDetail')).not.toBeInTheDocument()
})
})
diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/provider-card-actions.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/provider-card-actions.tsx
index ea0b6234854..b2511b18670 100644
--- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/provider-card-actions.tsx
+++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/provider-card-actions.tsx
@@ -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'
diff --git a/web/app/components/plugins/plugin-detail-panel/__tests__/detail-header.spec.tsx b/web/app/components/plugins/plugin-detail-panel/__tests__/detail-header.spec.tsx
index 05c9d4bdc6f..375f9f15b23 100644
--- a/web/app/components/plugins/plugin-detail-panel/__tests__/detail-header.spec.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/__tests__/detail-header.spec.tsx
@@ -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: () => ,
}))
-// Enhanced operation-dropdown mock
-vi.mock('../operation-dropdown', () => ({
- default: ({
- onInfo,
- onCheckVersion,
- onRemove,
- onViewReadme,
- }: {
- onInfo: () => void
- onCheckVersion: () => void
- onRemove: () => void
- onViewReadme?: () => void
- }) => (
-
-
-
-
- {onViewReadme && }
-
- ),
-}))
-
// 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()
+ 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()
- 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()
+ 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()
- 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()
- const removeBtn = screen.getByTestId('remove-btn')
- fireEvent.click(removeBtn)
+ openActionsMenu()
- expect(removeBtn)!.toBeInTheDocument()
- })
-
- it('should have uninstallPlugin mock defined', () => {
- render()
-
- 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()
- expect(screen.getByTestId('remove-btn'))!.toBeInTheDocument()
+ openActionsMenu()
+ expect(screen.getByText('plugin.detailPanel.operation.remove')).toBeInTheDocument()
})
it('should render correctly for tool plugin delete', () => {
render()
- expect(screen.getByTestId('remove-btn'))!.toBeInTheDocument()
+ openActionsMenu()
+ expect(screen.getByText('plugin.detailPanel.operation.remove')).toBeInTheDocument()
})
})
@@ -772,20 +758,23 @@ describe('DetailHeader', () => {
})
render()
- 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()
- 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()
- 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()
- 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()
- 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()
- 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()
- 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()
- 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()
- 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()
- 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()
- 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()
- 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()
- 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()
+ const detail = createPluginDetail({
+ source: PluginSource.github,
+ meta: { repo: 'owner/repo', version: 'v1.0.0', package: 'pkg' },
+ })
+ render()
- 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()
+ const detail = createPluginDetail({
+ source: PluginSource.github,
+ meta: { repo: 'owner/repo', version: 'v1.0.0', package: 'pkg' },
+ })
+ render()
- 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()
- expect(screen.getByTestId('info-btn'))!.toBeInTheDocument()
+ openActionsMenu()
+ expect(screen.getByText('plugin.detailPanel.operation.info')).toBeInTheDocument()
})
})
})
diff --git a/web/app/components/plugins/plugin-detail-panel/__tests__/operation-dropdown.spec.tsx b/web/app/components/plugins/plugin-detail-panel/__tests__/operation-dropdown.spec.tsx
index d476d7c85cf..3e0932e0ecc 100644
--- a/web/app/components/plugins/plugin-detail-panel/__tests__/operation-dropdown.spec.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/__tests__/operation-dropdown.spec.tsx
@@ -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 }) => (
- {children}
- ),
- DropdownMenuTrigger: ({ children, className }: { children: ReactNode, className?: string }) => (
-
- ),
- DropdownMenuContent: ({ children, popupClassName }: { children: ReactNode, popupClassName?: string }) => (
- {children}
- ),
- 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, children)
- return {children}
- },
- DropdownMenuSeparator: ({ className }: { className?: string }) =>
,
-}))
+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()
- expect(screen.getByTestId('dropdown-trigger')).toBeInTheDocument()
+ expect(screen.getByRole('button', { name: 'plugin.detailPanel.operation.moreActions' })).toBeInTheDocument()
})
- it('should render dropdown content', () => {
- render()
-
- expect(screen.getByTestId('dropdown-content')).toBeInTheDocument()
- })
-
- it('should render figma-aligned dropdown surface and rows', () => {
- render()
-
- 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()
+ openDropdown()
+
expect(screen.getByText('plugin.detailPanel.operation.info')).toBeInTheDocument()
- })
-
- it('should render check update option for github source', () => {
- render()
-
expect(screen.getByText('plugin.detailPanel.operation.checkUpdate')).toBeInTheDocument()
- })
-
- it('should render view detail option for github source with marketplace enabled', () => {
- render()
-
expect(screen.getByText('plugin.detailPanel.operation.viewDetail')).toBeInTheDocument()
- })
-
- it('should render view detail option for marketplace source', () => {
- render()
-
- expect(screen.getByText('plugin.detailPanel.operation.viewDetail')).toBeInTheDocument()
- })
-
- it('should render view README option when provided', () => {
- render()
-
- expect(screen.getByText('plugin.detailPanel.operation.viewReadme')).toBeInTheDocument()
- })
-
- it('should render view README below view marketplace', () => {
- render()
-
- 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()
-
- expect(screen.queryByText('plugin.detailPanel.operation.viewReadme')).not.toBeInTheDocument()
- })
-
- it('should always render remove option', () => {
- render()
-
expect(screen.getByText('plugin.detailPanel.operation.remove')).toBeInTheDocument()
})
- it('should render remove option with normal text color', () => {
- render()
-
- 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()
-
- 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()
+ openDropdown()
+
expect(screen.queryByText('plugin.detailPanel.operation.info')).not.toBeInTheDocument()
- })
-
- it('should not render check update option for marketplace source', () => {
- render()
-
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()
+
+ openDropdown()
+ expect(screen.queryByText('plugin.detailPanel.operation.viewReadme')).not.toBeInTheDocument()
+
+ unmount()
+ render()
+ openDropdown()
+ expect(screen.getByText('plugin.detailPanel.operation.viewReadme')).toBeInTheDocument()
+ })
+
+ it('should not render marketplace detail when source cannot open a detail page', () => {
render()
+ 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()
+ it('should not render the trigger when every action is unavailable', () => {
+ render(
+ ,
+ )
- 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()
-
- 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()
+ 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()
+ openDropdown()
fireEvent.click(screen.getByText('plugin.detailPanel.operation.checkUpdate'))
expect(mockOnCheckVersion).toHaveBeenCalledTimes(1)
})
- it('should call onRemove when remove option is clicked', () => {
- render()
+ it('should call onRemove when the remove action is clicked', () => {
+ render()
+ 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()
+ 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()
+ 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(, false)
- sources.forEach((source) => {
- const { unmount } = render(
- ,
- )
- 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(
- ,
- )
- 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()
})
})
})
diff --git a/web/app/components/plugins/plugin-detail-panel/detail-header/__tests__/index.spec.tsx b/web/app/components/plugins/plugin-detail-panel/detail-header/__tests__/index.spec.tsx
index e809573a7ce..3fdd82f7091 100644
--- a/web/app/components/plugins/plugin-detail-panel/detail-header/__tests__/index.spec.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/detail-header/__tests__/index.spec.tsx
@@ -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 }) => {detailUrl}
,
-}))
-
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()
})
diff --git a/web/app/components/plugins/plugin-detail-panel/detail-header/index.tsx b/web/app/components/plugins/plugin-detail-panel/detail-header/index.tsx
index a69444ee6a0..e8f1e63f7aa 100644
--- a/web/app/components/plugins/plugin-detail-panel/detail-header/index.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/detail-header/index.tsx
@@ -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'
diff --git a/web/app/components/plugins/plugin-detail-panel/operation-dropdown.tsx b/web/app/components/plugins/plugin-detail-panel/operation-dropdown.tsx
index a18807b0bb9..d6a34c50b78 100644
--- a/web/app/components/plugins/plugin-detail-panel/operation-dropdown.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/operation-dropdown.tsx
@@ -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 = ({
+export function OperationDropdown({
source,
detailUrl,
onInfo,
@@ -51,7 +46,7 @@ const OperationDropdown: FC = ({
destructiveRemove = false,
showCheckVersion = true,
showRemove = true,
-}) => {
+}: OperationDropdownProps) {
const { t } = useTranslation()
const { data: enable_marketplace } = useSuspenseQuery({
...systemFeaturesQueryOptions(),
@@ -70,58 +65,55 @@ const OperationDropdown: FC = ({
-
+
{showInfo && (
-
- {t('detailPanel.operation.info', { ns: 'plugin' })}
+
+ {t('detailPanel.operation.info', { ns: 'plugin' })}
)}
{showCheckVersionAction && (
-
- {t('detailPanel.operation.checkUpdate', { ns: 'plugin' })}
+
+ {t('detailPanel.operation.checkUpdate', { ns: 'plugin' })}
)}
{showMarketplaceDetail && (
-
- )}
+
- {t('detailPanel.operation.viewDetail', { ns: 'plugin' })}
+ {t('detailPanel.operation.viewDetail', { ns: 'plugin' })}
-
+
)}
{onViewReadme && (
-
- {t('detailPanel.operation.viewReadme', { ns: 'plugin' })}
+
+ {t('detailPanel.operation.viewReadme', { ns: 'plugin' })}
)}
{showSeparator && (
-
+
)}
{showRemoveAction && (
-
+
{t('detailPanel.operation.remove', { ns: 'plugin' })}
@@ -130,4 +122,3 @@ const OperationDropdown: FC = ({
)
}
-export default React.memo(OperationDropdown)
diff --git a/web/i18n/ar-TN/plugin.json b/web/i18n/ar-TN/plugin.json
index f46ef50eb18..181ce8e01a2 100644
--- a/web/i18n/ar-TN/plugin.json
+++ b/web/i18n/ar-TN/plugin.json
@@ -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": "قم بالتحديث للوصول إلى أحدث النماذج.",
diff --git a/web/i18n/de-DE/plugin.json b/web/i18n/de-DE/plugin.json
index bb8700bbae0..d28efaa2206 100644
--- a/web/i18n/de-DE/plugin.json
+++ b/web/i18n/de-DE/plugin.json
@@ -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.",
diff --git a/web/i18n/en-US/plugin.json b/web/i18n/en-US/plugin.json
index 121789b0071..4ebcfaed421 100644
--- a/web/i18n/en-US/plugin.json
+++ b/web/i18n/en-US/plugin.json
@@ -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.",
diff --git a/web/i18n/es-ES/plugin.json b/web/i18n/es-ES/plugin.json
index 3db287f6322..9c5792d1f02 100644
--- a/web/i18n/es-ES/plugin.json
+++ b/web/i18n/es-ES/plugin.json
@@ -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.",
diff --git a/web/i18n/fa-IR/plugin.json b/web/i18n/fa-IR/plugin.json
index 0ff89d623c2..702125a4996 100644
--- a/web/i18n/fa-IR/plugin.json
+++ b/web/i18n/fa-IR/plugin.json
@@ -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": "بهروزرسانی کنید تا به جدیدترین مدلها دسترسی پیدا کنید.",
diff --git a/web/i18n/fr-FR/plugin.json b/web/i18n/fr-FR/plugin.json
index 55b244e6742..97ab7c6ea0b 100644
--- a/web/i18n/fr-FR/plugin.json
+++ b/web/i18n/fr-FR/plugin.json
@@ -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.",
diff --git a/web/i18n/hi-IN/plugin.json b/web/i18n/hi-IN/plugin.json
index 844ee11541e..4c9879d5dc0 100644
--- a/web/i18n/hi-IN/plugin.json
+++ b/web/i18n/hi-IN/plugin.json
@@ -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": "नवीनतम मॉडल तक पहुँचने के लिए अपडेट करें।",
diff --git a/web/i18n/id-ID/plugin.json b/web/i18n/id-ID/plugin.json
index 362d57afd5a..a98c216d1a3 100644
--- a/web/i18n/id-ID/plugin.json
+++ b/web/i18n/id-ID/plugin.json
@@ -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.",
diff --git a/web/i18n/it-IT/plugin.json b/web/i18n/it-IT/plugin.json
index 3727cdfad46..2518d1b5099 100644
--- a/web/i18n/it-IT/plugin.json
+++ b/web/i18n/it-IT/plugin.json
@@ -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.",
diff --git a/web/i18n/ja-JP/plugin.json b/web/i18n/ja-JP/plugin.json
index 9e828f8fd29..b337cd1ada2 100644
--- a/web/i18n/ja-JP/plugin.json
+++ b/web/i18n/ja-JP/plugin.json
@@ -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": "最新のモデルにアクセスするために更新してください。",
diff --git a/web/i18n/ko-KR/plugin.json b/web/i18n/ko-KR/plugin.json
index 62b51478852..5cf37f6039f 100644
--- a/web/i18n/ko-KR/plugin.json
+++ b/web/i18n/ko-KR/plugin.json
@@ -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": "최신 모델에 액세스하려면 업데이트하세요.",
diff --git a/web/i18n/nl-NL/plugin.json b/web/i18n/nl-NL/plugin.json
index 9429fd3f6a4..02482550973 100644
--- a/web/i18n/nl-NL/plugin.json
+++ b/web/i18n/nl-NL/plugin.json
@@ -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.",
diff --git a/web/i18n/pl-PL/plugin.json b/web/i18n/pl-PL/plugin.json
index 40ba6e90a70..251050d729e 100644
--- a/web/i18n/pl-PL/plugin.json
+++ b/web/i18n/pl-PL/plugin.json
@@ -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.",
diff --git a/web/i18n/pt-BR/plugin.json b/web/i18n/pt-BR/plugin.json
index 3c031fc7c0c..0c688fb25bc 100644
--- a/web/i18n/pt-BR/plugin.json
+++ b/web/i18n/pt-BR/plugin.json
@@ -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.",
diff --git a/web/i18n/ro-RO/plugin.json b/web/i18n/ro-RO/plugin.json
index 2181ed66e64..437d59a4fb6 100644
--- a/web/i18n/ro-RO/plugin.json
+++ b/web/i18n/ro-RO/plugin.json
@@ -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.",
diff --git a/web/i18n/ru-RU/plugin.json b/web/i18n/ru-RU/plugin.json
index d25e50885af..a86914f6eee 100644
--- a/web/i18n/ru-RU/plugin.json
+++ b/web/i18n/ru-RU/plugin.json
@@ -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": "Обновите для доступа к последним моделям.",
diff --git a/web/i18n/sl-SI/plugin.json b/web/i18n/sl-SI/plugin.json
index be30977a79c..fa7e41c3a9a 100644
--- a/web/i18n/sl-SI/plugin.json
+++ b/web/i18n/sl-SI/plugin.json
@@ -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.",
diff --git a/web/i18n/th-TH/plugin.json b/web/i18n/th-TH/plugin.json
index fa09b838018..a1de1513efb 100644
--- a/web/i18n/th-TH/plugin.json
+++ b/web/i18n/th-TH/plugin.json
@@ -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": "อัปเดตเพื่อเข้าถึงโมเดลล่าสุด",
diff --git a/web/i18n/tr-TR/plugin.json b/web/i18n/tr-TR/plugin.json
index 080c13c0416..62dea7a9ad0 100644
--- a/web/i18n/tr-TR/plugin.json
+++ b/web/i18n/tr-TR/plugin.json
@@ -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.",
diff --git a/web/i18n/uk-UA/plugin.json b/web/i18n/uk-UA/plugin.json
index b085f6261d5..4cdbd56f92f 100644
--- a/web/i18n/uk-UA/plugin.json
+++ b/web/i18n/uk-UA/plugin.json
@@ -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": "Оновіть, щоб отримати доступ до найновіших моделей.",
diff --git a/web/i18n/vi-VN/plugin.json b/web/i18n/vi-VN/plugin.json
index fa711bb99ec..b8027d9c580 100644
--- a/web/i18n/vi-VN/plugin.json
+++ b/web/i18n/vi-VN/plugin.json
@@ -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.",
diff --git a/web/i18n/zh-Hans/plugin.json b/web/i18n/zh-Hans/plugin.json
index cbbde8c3391..638b94e4fd1 100644
--- a/web/i18n/zh-Hans/plugin.json
+++ b/web/i18n/zh-Hans/plugin.json
@@ -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": "更新以获取最新模型。",
diff --git a/web/i18n/zh-Hant/plugin.json b/web/i18n/zh-Hant/plugin.json
index ba4c3463e95..6a6afa8e5ef 100644
--- a/web/i18n/zh-Hant/plugin.json
+++ b/web/i18n/zh-Hant/plugin.json
@@ -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": "更新以取得最新模型。",