diff --git a/web/app/components/integrations/__tests__/page.spec.tsx b/web/app/components/integrations/__tests__/page.spec.tsx index 366c7cb05d7..72c98517f75 100644 --- a/web/app/components/integrations/__tests__/page.spec.tsx +++ b/web/app/components/integrations/__tests__/page.spec.tsx @@ -1,4 +1,5 @@ -import { fireEvent, screen, within } from '@testing-library/react' +import { screen, within } from '@testing-library/react' +import userEvent from '@testing-library/user-event' import { renderWithNuqs } from '@/test/nuqs-testing' import IntegrationsPage from '../page' @@ -476,10 +477,11 @@ describe('IntegrationsPage', () => { ['extension', 'empty marketplace', '/plugins/extension'], ] as const)( 'opens the %s marketplace path from integrations', - (section, buttonName, marketplacePath) => { + async (section, buttonName, marketplacePath) => { + const user = userEvent.setup() renderIntegrationsPage({ section }) - fireEvent.click(screen.getByRole('button', { name: buttonName })) + await user.click(screen.getByRole('button', { name: buttonName })) expect(mockWindowOpen).toHaveBeenCalledWith( expect.stringContaining(`${marketplacePath}?source=`), @@ -490,11 +492,12 @@ describe('IntegrationsPage', () => { }, ) - it('passes marketplace platform paths to external marketplace callbacks', () => { + it('passes marketplace platform paths to external marketplace callbacks', async () => { + const user = userEvent.setup() const onSwitchToMarketplace = vi.fn() renderIntegrationsPage({ section: 'trigger' }, { onSwitchToMarketplace }) - fireEvent.click(screen.getByRole('button', { name: 'empty marketplace' })) + await user.click(screen.getByRole('button', { name: 'empty marketplace' })) expect(onSwitchToMarketplace).toHaveBeenCalledWith('/plugins/trigger') expect(mockRouterPush).not.toHaveBeenCalled() @@ -633,60 +636,8 @@ describe('IntegrationsPage', () => { ).toBe(Node.DOCUMENT_POSITION_FOLLOWING) }) - it('uses hover-only arrows for the tools parent icon', () => { - const view = renderIntegrationsPage({ section: 'provider' }) - - const collapsedToolsButton = screen.getByRole('button', { name: 'common.menus.tools' }) - const collapsedDisclosureIcon = collapsedToolsButton.querySelector( - 'svg[viewBox="0 0 12 14.0003"]', - ) - - expect(collapsedToolsButton).toHaveAttribute('aria-expanded', 'false') - expect(collapsedDisclosureIcon).toBeInTheDocument() - expect(collapsedDisclosureIcon).toHaveClass('h-3.5', 'w-3', 'group-hover:hidden') - expect(collapsedToolsButton.querySelector('[data-icon="MagicBox"]')).not.toBeInTheDocument() - expect( - collapsedToolsButton.querySelector('.i-custom-vender-solid-mediaAndDevices-magic-box'), - ).not.toBeInTheDocument() - expect( - collapsedToolsButton.querySelector('.i-custom-vender-plugin-box-sparkle-fill'), - ).not.toBeInTheDocument() - expect(collapsedToolsButton.querySelector('.i-ri-arrow-down-s-line')).toHaveClass( - 'hidden', - 'group-hover:inline-block', - ) - expect(collapsedToolsButton.querySelector('.i-ri-arrow-up-s-line')).not.toBeInTheDocument() - expect( - screen.queryByRole('link', { name: 'common.toolsPage.toolPlugin' }), - ).not.toBeInTheDocument() - - view.unmount() - renderIntegrationsPage({ section: 'mcp' }) - - const expandedToolsButton = screen.getByRole('button', { name: 'common.menus.tools' }) - const expandedDisclosureIcon = expandedToolsButton.querySelector( - 'svg[viewBox="0 0 12 14.0003"]', - ) - - expect(expandedToolsButton).toHaveAttribute('aria-expanded', 'true') - expect(expandedToolsButton).not.toHaveClass('bg-state-base-active') - expect(expandedToolsButton).not.toHaveAttribute('aria-current') - expect(expandedDisclosureIcon).toBeInTheDocument() - expect(expandedToolsButton.querySelector('.i-ri-arrow-up-s-line')).toHaveClass( - 'hidden', - 'group-hover:inline-block', - ) - expect(expandedToolsButton.querySelector('.i-ri-arrow-down-s-line')).not.toBeInTheDocument() - expect( - expandedToolsButton.querySelector('.i-custom-vender-integrations-tools-active'), - ).not.toBeInTheDocument() - expect(screen.getByRole('link', { name: 'common.toolsPage.toolPlugin' })).toHaveAttribute( - 'href', - '/integrations/tools/built-in', - ) - }) - - it('toggles the tools submenu without other nav items closing it', () => { + it('toggles the tools submenu from the keyboard without other nav items closing it', async () => { + const user = userEvent.setup() const onSectionChange = vi.fn() renderWithNuqs() @@ -699,31 +650,34 @@ describe('IntegrationsPage', () => { expect(toolsButton).toHaveAttribute('aria-expanded', 'false') expect(screen.queryByRole('button', { name: 'MCP' })).not.toBeInTheDocument() - fireEvent.click(toolsButton) + toolsButton.focus() + await user.keyboard('{Enter}') expect(onSectionChange).toHaveBeenCalledWith('builtin') expect(toolsButton).toHaveAttribute('aria-expanded', 'true') expect(screen.getByRole('button', { name: 'common.toolsPage.toolPlugin' })).toBeInTheDocument() expect(screen.getByRole('button', { name: 'MCP' })).toBeInTheDocument() - fireEvent.click(screen.getByRole('button', { name: 'common.settings.provider' })) + await user.click(screen.getByRole('button', { name: 'common.settings.provider' })) expect(onSectionChange).toHaveBeenCalledWith('provider') expect(toolsButton).toHaveAttribute('aria-expanded', 'true') expect(screen.getByRole('button', { name: 'MCP' })).toBeInTheDocument() - fireEvent.click(toolsButton) + toolsButton.focus() + await user.keyboard(' ') expect(toolsButton).toHaveAttribute('aria-expanded', 'false') expect(screen.queryByRole('button', { name: 'MCP' })).not.toBeInTheDocument() expect(onSectionChange).toHaveBeenCalledTimes(2) }) - it('keeps custom, workflow, and MCP tool entries visible without manage permissions', () => { + it('keeps custom, workflow, and MCP tool entries visible without manage permissions', async () => { + const user = userEvent.setup() mockAppContextState.workspacePermissionKeys = ['mcp.manage'] renderIntegrationsPage(undefined, { section: 'provider', onSectionChange: vi.fn() }) - fireEvent.click(screen.getByRole('button', { name: 'common.menus.tools' })) + await user.click(screen.getByRole('button', { name: 'common.menus.tools' })) expect(screen.getByRole('button', { name: 'common.toolsPage.toolPlugin' })).toBeInTheDocument() expect(screen.getByRole('button', { name: 'MCP' })).toBeInTheDocument() @@ -735,15 +689,17 @@ describe('IntegrationsPage', () => { ).toBeInTheDocument() }) - it('opens tools to the tools plugin page when the parent tools nav is clicked', () => { + it('opens tools to the tools plugin page when the parent tools nav is clicked', async () => { + const user = userEvent.setup() renderIntegrationsPage(undefined, 'provider') - fireEvent.click(screen.getByRole('button', { name: 'common.menus.tools' })) + await user.click(screen.getByRole('button', { name: 'common.menus.tools' })) expect(mockRouterPush).toHaveBeenCalledWith('/integrations/tools/built-in') }) - it('keeps the tools disclosure independent from route section changes', () => { + it('keeps the tools disclosure independent from route section changes', async () => { + const user = userEvent.setup() const view = renderIntegrationsPage(undefined, 'mcp') expect(screen.getByTestId('tool-provider-list')).toHaveAttribute('data-mounted-category', 'mcp') @@ -754,7 +710,7 @@ describe('IntegrationsPage', () => { expect(screen.getByRole('link', { name: 'common.toolsPage.toolPlugin' })).toBeInTheDocument() expect(screen.getByRole('link', { name: 'MCP' })).toBeInTheDocument() - fireEvent.click(screen.getByRole('button', { name: 'common.menus.tools' })) + await user.click(screen.getByRole('button', { name: 'common.menus.tools' })) expect(screen.getByTestId('tool-provider-list')).toHaveAttribute('data-mounted-category', 'mcp') expect(screen.getByRole('button', { name: 'common.menus.tools' })).toHaveAttribute( @@ -938,10 +894,11 @@ describe('IntegrationsPage', () => { }, ) - it('opens the integrations marketplace path from the install dropdown marketplace action', () => { + it('opens the integrations marketplace path from the install dropdown marketplace action', async () => { + const user = userEvent.setup() renderIntegrationsPage({ section: 'builtin' }) - fireEvent.click(screen.getByRole('button', { name: 'plugin install' })) + await user.click(screen.getByRole('button', { name: 'plugin install' })) expect(mockWindowOpen).toHaveBeenCalledWith( expect.stringContaining('/plugins/tool?source='), @@ -985,27 +942,18 @@ describe('IntegrationsPage', () => { expect(screen.queryByTestId('update-setting-dialog')).not.toBeInTheDocument() }) - it('opens the sidebar plugin permissions quick settings and updates permissions', () => { + it('opens the sidebar plugin permissions quick settings and updates permissions', async () => { + const user = userEvent.setup() renderIntegrationsPage({ section: 'provider' }) - fireEvent.click(screen.getByRole('button', { name: 'plugin.privilege.permissions' })) + await user.click(screen.getByRole('button', { name: 'plugin.privilege.permissions' })) - expect(screen.getAllByText('plugin.privilege.permissions').length).toBeGreaterThan(0) - expect(screen.getByText('plugin.privilege.quickWhoCanInstall')).toBeInTheDocument() - expect(screen.getByText('plugin.privilege.quickWhoCanDebug')).toBeInTheDocument() + const dialog = screen.getByRole('dialog', { name: 'plugin.privilege.permissions' }) + expect(within(dialog).getByText('plugin.privilege.quickWhoCanInstall')).toBeInTheDocument() + expect(within(dialog).getByText('plugin.privilege.quickWhoCanDebug')).toBeInTheDocument() - const dialog = screen.getByRole('dialog') - expect( - within(dialog).getByText('plugin.privilege.permissions').closest('.w-\\[360px\\]'), - ).toHaveClass('rounded-2xl', 'shadow-2xl') - expect( - screen.getByRole('radio', { - name: 'plugin.privilege.quickWhoCanInstall: plugin.privilege.everyone', - }), - ).toHaveClass('w-[104px]', 'h-8') - - fireEvent.click( - screen.getByRole('radio', { + await user.click( + within(dialog).getByRole('radio', { name: 'plugin.privilege.quickWhoCanInstall: plugin.privilege.noone', }), ) @@ -1014,6 +962,12 @@ describe('IntegrationsPage', () => { install_permission: 'noone', debug_permission: 'admins', }) + + await user.click(within(dialog).getByRole('button', { name: 'common.operation.close' })) + + expect( + screen.queryByRole('dialog', { name: 'plugin.privilege.permissions' }), + ).not.toBeInTheDocument() }) it('hides the sidebar plugin permissions quick settings when permission management is unavailable', () => { diff --git a/web/app/components/integrations/page.tsx b/web/app/components/integrations/page.tsx index c36972e5e01..7b7dc01241c 100644 --- a/web/app/components/integrations/page.tsx +++ b/web/app/components/integrations/page.tsx @@ -4,6 +4,7 @@ import type { CSSProperties, ReactNode } from 'react' import type { IntegrationSection } from '@/app/components/integrations/routes' import type { DocPathWithoutLang } from '@/types/doc-paths' import { cn } from '@langgenius/dify-ui/cn' +import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from '@langgenius/dify-ui/collapsible' import { ScrollArea } from '@langgenius/dify-ui/scroll-area' import { useState } from 'react' import { useTranslation } from 'react-i18next' @@ -191,10 +192,9 @@ export default function IntegrationsPage({ router.push(buildIntegrationPath(nextSection)) } - const handleToggleTools = () => { - const willExpand = !isToolsExpanded - setIsToolsExpanded(willExpand) - if (willExpand && section !== 'builtin') handleSelectSection('builtin') + const handleToolsOpenChange = (open: boolean) => { + setIsToolsExpanded(open) + if (open && section !== 'builtin') handleSelectSection('builtin') } const toolsNavItemClassName = cn( integrationSidebarNavItemClassName, @@ -204,12 +204,8 @@ export default function IntegrationsPage({ const toolsNavItemContent = ( <> - - {isToolsExpanded ? ( - - ) : ( - - )} + + {t(($) => $['menus.tools'], { ns: 'common' })} @@ -254,29 +250,27 @@ export default function IntegrationsPage({ onSelect={onSectionChange} section={section} /> -
- - {isToolsExpanded && ( -
- {toolItems.map((item) => ( - - ))} -
- )} -
+ + + {toolItems.map((item) => ( + + ))} + + ({ CopyCheck: () => , })) -vi.mock('@/app/components/base/action-button', () => ({ - default: ({ children, onClick, ...props }: React.ButtonHTMLAttributes) => ( - - ), -})) - const mockCopy = vi.fn() vi.mock('copy-to-clipboard', () => ({ default: (...args: unknown[]) => mockCopy(...args), diff --git a/web/app/components/plugins/base/key-value-item.tsx b/web/app/components/plugins/base/key-value-item.tsx index 3da2420113f..9e288e5e821 100644 --- a/web/app/components/plugins/base/key-value-item.tsx +++ b/web/app/components/plugins/base/key-value-item.tsx @@ -1,12 +1,10 @@ 'use client' -import type { FC } from 'react' +import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import copy from 'copy-to-clipboard' -import * as React from 'react' import { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' -import ActionButton from '@/app/components/base/action-button' import { CopyCheck } from '../../base/icons/src/vender/line/files' type Props = Readonly<{ @@ -17,13 +15,13 @@ type Props = Readonly<{ valueMaxWidthClassName?: string }> -const KeyValueItem: FC = ({ +function KeyValueItem({ label, labelWidthClassName = 'w-10', value, maskedValue, valueMaxWidthClassName = 'max-w-[162px]', -}) => { +}: Props) { const { t } = useTranslation() const [isCopied, setIsCopied] = useState(false) const handleCopy = useCallback(() => { @@ -63,7 +61,12 @@ const KeyValueItem: FC = ({ + } /> {copyLabel} @@ -82,4 +85,4 @@ const KeyValueItem: FC = ({ ) } -export default React.memo(KeyValueItem) +export default KeyValueItem diff --git a/web/app/components/plugins/plugin-page/__tests__/debug-info.spec.tsx b/web/app/components/plugins/plugin-page/__tests__/debug-info.spec.tsx index 2e97fd5ec60..177b63909bf 100644 --- a/web/app/components/plugins/plugin-page/__tests__/debug-info.spec.tsx +++ b/web/app/components/plugins/plugin-page/__tests__/debug-info.spec.tsx @@ -76,7 +76,7 @@ describe('DebugInfo', () => { 'rounded-2xl', 'shadow-2xl', ) - expect(screen.getByRole('link')).toHaveAttribute( + expect(screen.getByRole('link', { name: 'plugin.debugInfo.viewDocs' })).toHaveAttribute( 'href', 'https://docs.example.com/develop-plugin/features-and-specs/plugin-types/remote-debug-a-plugin', ) diff --git a/web/app/components/plugins/plugin-page/debug-info.tsx b/web/app/components/plugins/plugin-page/debug-info.tsx index f6c62075bf6..bc36b4cdd35 100644 --- a/web/app/components/plugins/plugin-page/debug-info.tsx +++ b/web/app/components/plugins/plugin-page/debug-info.tsx @@ -4,7 +4,6 @@ import type { ComponentProps, ReactNode } from 'react' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' -import { RiArrowRightUpLine, RiBugLine } from '@remixicon/react' import { useTranslation } from 'react-i18next' import { useDocLink } from '@/context/i18n' import { useDebugKey } from '@/service/use-plugins' @@ -29,7 +28,7 @@ function DebugInfo({ const { t } = useTranslation() const docLink = useDocLink() const { data: info, isLoading } = useDebugKey() - const trigger = triggerContent ?? + const trigger = triggerContent ?? const triggerClassNames = cn( !triggerClassName && 'size-full p-2 text-components-button-secondary-text', triggerClassName, @@ -73,10 +72,10 @@ function DebugInfo({ )} target="_blank" rel="noopener noreferrer" - className="flex cursor-pointer items-center gap-1 system-xs-regular text-text-accent" + className="flex cursor-pointer items-center gap-1 rounded-xs system-xs-regular text-text-accent outline-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid" > {t(($) => $[`${i18nPrefix}.viewDocs`], { ns: 'plugin' })} - + diff --git a/web/app/components/plugins/plugin-page/plugin-sidecar-panel.tsx b/web/app/components/plugins/plugin-page/plugin-sidecar-panel.tsx index 7dbcc250160..83fd3c87a14 100644 --- a/web/app/components/plugins/plugin-page/plugin-sidecar-panel.tsx +++ b/web/app/components/plugins/plugin-page/plugin-sidecar-panel.tsx @@ -1,7 +1,8 @@ 'use client' import type { ReactNode } from 'react' -import { PopoverClose } from '@langgenius/dify-ui/popover' +import { Button } from '@langgenius/dify-ui/button' +import { PopoverClose, PopoverTitle } from '@langgenius/dify-ui/popover' import { useTranslation } from 'react-i18next' type PluginSidecarPanelProps = { @@ -18,18 +19,20 @@ export function PluginSidecarPanel({ children, footer, title }: PluginSidecarPan
-
{title}
+ + {title} +
$['operation.close'], { ns: 'common' })} - className="absolute top-2.5 right-2.5 flex size-8 items-center justify-center rounded-lg text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary" + className="absolute top-2.5 right-2.5 size-8 p-0 text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary" > - + } />