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": "更新以取得最新模型。",