mirror of
https://github.com/langgenius/dify.git
synced 2026-07-20 09:38:32 +08:00
refactor(web): align integrations sidebar components (#39202)
Co-authored-by: Jingyi <jingyi.qi@dify.ai>
This commit is contained in:
parent
63ca2b94b5
commit
fa65f45bff
@ -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(<IntegrationsPage section="provider" onSectionChange={onSectionChange} />)
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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 = (
|
||||
<>
|
||||
<span aria-hidden className="flex size-5 shrink-0 items-center justify-center">
|
||||
<ToolsDisclosureIcon className="h-3.5 w-3 group-hover:hidden" />
|
||||
{isToolsExpanded ? (
|
||||
<span className="i-ri-arrow-up-s-line hidden size-4 group-hover:inline-block" />
|
||||
) : (
|
||||
<span className="i-ri-arrow-down-s-line hidden size-4 group-hover:inline-block" />
|
||||
)}
|
||||
<ToolsDisclosureIcon className="h-3.5 w-3 group-hover:hidden group-focus-visible:hidden" />
|
||||
<span className="i-ri-arrow-down-s-line hidden size-4 transition-transform duration-100 ease-out group-hover:inline-block group-focus-visible:inline-block group-data-panel-open:rotate-180 motion-reduce:transition-none" />
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 truncate">
|
||||
{t(($) => $['menus.tools'], { ns: 'common' })}
|
||||
@ -254,29 +250,27 @@ export default function IntegrationsPage({
|
||||
onSelect={onSectionChange}
|
||||
section={section}
|
||||
/>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
<Collapsible open={isToolsExpanded} onOpenChange={handleToolsOpenChange}>
|
||||
<CollapsibleTrigger
|
||||
aria-label={t(($) => $['menus.tools'], { ns: 'common' })}
|
||||
aria-expanded={isToolsExpanded}
|
||||
className={cn(toolsNavItemClassName, 'border-none bg-transparent')}
|
||||
onClick={handleToggleTools}
|
||||
className={cn(
|
||||
toolsNavItemClassName,
|
||||
'border-none bg-transparent data-panel-open:text-components-menu-item-text',
|
||||
)}
|
||||
>
|
||||
{toolsNavItemContent}
|
||||
</button>
|
||||
{isToolsExpanded && (
|
||||
<div className="relative space-y-px before:absolute before:top-[-1px] before:bottom-0 before:left-[17.5px] before:w-px before:bg-divider-regular">
|
||||
{toolItems.map((item) => (
|
||||
<IntegrationSidebarNavItem
|
||||
key={item.label}
|
||||
item={item}
|
||||
onSelect={onSectionChange}
|
||||
section={section}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsiblePanel className="relative space-y-px before:absolute before:top-[-1px] before:bottom-0 before:left-[17.5px] before:w-px before:bg-divider-regular">
|
||||
{toolItems.map((item) => (
|
||||
<IntegrationSidebarNavItem
|
||||
key={item.label}
|
||||
item={item}
|
||||
onSelect={onSectionChange}
|
||||
section={section}
|
||||
/>
|
||||
))}
|
||||
</CollapsiblePanel>
|
||||
</Collapsible>
|
||||
<IntegrationSidebarNavItem
|
||||
item={dataSourceItem}
|
||||
onSelect={onSectionChange}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
export const integrationSidebarNavItemClassName =
|
||||
'flex h-8 w-full items-center gap-2 rounded-lg py-1 pr-1 pl-2 text-left system-sm-medium transition-colors'
|
||||
'flex h-8 w-full items-center gap-2 rounded-lg py-1 pr-1 pl-2 text-left system-sm-medium outline-hidden transition-colors focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-state-accent-solid'
|
||||
export const integrationSidebarActiveNavItemClassName =
|
||||
'bg-state-base-active text-components-menu-item-text-active'
|
||||
export const integrationSidebarInactiveNavItemClassName =
|
||||
|
||||
@ -6,14 +6,6 @@ vi.mock('../../../base/icons/src/vender/line/files', () => ({
|
||||
CopyCheck: () => <span />,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/action-button', () => ({
|
||||
default: ({ children, onClick, ...props }: React.ButtonHTMLAttributes<HTMLButtonElement>) => (
|
||||
<button onClick={onClick} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
const mockCopy = vi.fn()
|
||||
vi.mock('copy-to-clipboard', () => ({
|
||||
default: (...args: unknown[]) => mockCopy(...args),
|
||||
|
||||
@ -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<Props> = ({
|
||||
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<Props> = ({
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<ActionButton aria-label={copyLabel} onClick={handleCopy}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
aria-label={copyLabel}
|
||||
className="size-6 p-0"
|
||||
onClick={handleCopy}
|
||||
>
|
||||
{isCopied ? (
|
||||
<CopyCheck aria-hidden className="size-3.5 shrink-0 text-text-tertiary" />
|
||||
) : (
|
||||
@ -72,7 +75,7 @@ const KeyValueItem: FC<Props> = ({
|
||||
className="i-ri-clipboard-line size-3.5 shrink-0 text-text-tertiary"
|
||||
/>
|
||||
)}
|
||||
</ActionButton>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<TooltipContent placement="top">{copyLabel}</TooltipContent>
|
||||
@ -82,4 +85,4 @@ const KeyValueItem: FC<Props> = ({
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(KeyValueItem)
|
||||
export default KeyValueItem
|
||||
|
||||
@ -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',
|
||||
)
|
||||
|
||||
@ -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 ?? <RiBugLine className="size-4" />
|
||||
const trigger = triggerContent ?? <span aria-hidden className="i-ri-bug-line size-4" />
|
||||
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"
|
||||
>
|
||||
<span>{t(($) => $[`${i18nPrefix}.viewDocs`], { ns: 'plugin' })}</span>
|
||||
<RiArrowRightUpLine className="size-3" />
|
||||
<span aria-hidden className="i-ri-arrow-right-up-line size-3" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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
|
||||
<div className="relative flex w-full shrink-0 flex-col gap-0.5 px-3 pt-3.5 pb-1">
|
||||
<div className="flex w-full shrink-0 items-start">
|
||||
<div className="flex min-w-0 flex-1 flex-col items-start pr-8 pl-1">
|
||||
<div className="w-full system-xl-semibold text-text-primary">{title}</div>
|
||||
<PopoverTitle className="w-full system-xl-semibold text-text-primary">
|
||||
{title}
|
||||
</PopoverTitle>
|
||||
</div>
|
||||
</div>
|
||||
<PopoverClose
|
||||
render={
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
variant="ghost"
|
||||
aria-label={t(($) => $['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"
|
||||
>
|
||||
<span aria-hidden className="i-ri-close-line size-4" />
|
||||
</button>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user