refactor(web): migrate plugin icon commands (#40648)

This commit is contained in:
yyh 2026-08-13 06:44:02 +00:00 committed by GitHub
parent 288766171d
commit d6f32a0267
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 162 additions and 257 deletions

View File

@ -1,60 +0,0 @@
@utility action-btn {
@apply inline-flex cursor-pointer items-center justify-center text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary;
&.action-btn-active {
@apply bg-state-accent-active text-text-accent hover:bg-state-accent-active-alt;
}
&.action-btn-disabled {
@apply text-text-disabled;
}
&.action-btn-destructive {
@apply bg-state-destructive-hover text-text-destructive;
}
}
@utility action-btn-hover {
@apply bg-state-base-hover;
}
@utility action-btn-disabled {
@apply cursor-not-allowed;
&.action-btn {
@apply text-text-disabled;
}
}
@utility action-btn-xl {
@apply h-9 w-9 rounded-lg p-2;
}
@utility action-btn-l {
@apply h-8 w-8 rounded-lg p-1.5;
}
@utility action-btn-m {
/* m is for the regular button */
@apply h-6 w-6 rounded-lg p-0.5;
}
@utility action-btn-s {
@apply h-5 w-5 rounded-[6px];
}
@utility action-btn-xs {
@apply h-4 w-4 rounded-sm p-0;
}
@utility action-btn-active {
&.action-btn {
@apply bg-state-accent-active text-text-accent hover:bg-state-accent-active-alt;
}
}
@utility action-btn-destructive {
&.action-btn {
@apply bg-state-destructive-hover text-text-destructive;
}
}

View File

@ -1,11 +1,10 @@
'use client'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import copy from 'copy-to-clipboard'
import { useCallback, useEffect, useId, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { CopyCheck } from '../../base/icons/src/vender/line/files'
type Props = Readonly<{
label: string
@ -67,21 +66,19 @@ function KeyValueItem({
<Tooltip>
<TooltipTrigger
render={
<Button
variant="ghost"
aria-label={copyButtonLabel}
className="size-6 p-0"
onClick={handleCopy}
>
<IconButton aria-label={copyButtonLabel} onClick={handleCopy}>
{isCopied ? (
<CopyCheck aria-hidden className="size-3.5 shrink-0 text-text-tertiary" />
<span
aria-hidden
className="i-custom-vender-line-files-copy-check size-3.5 shrink-0 text-text-tertiary"
/>
) : (
<span
aria-hidden
className="i-ri-clipboard-line size-3.5 shrink-0 text-text-tertiary"
/>
)}
</Button>
</IconButton>
}
/>
<TooltipContent placement="top">{tooltipLabel}</TooltipContent>

View File

@ -1,7 +1,7 @@
'use client'
import type { Ref } from 'react'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { useImperativeHandle, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import Divider from '@/app/components/base/divider'
@ -89,18 +89,18 @@ function SearchBox({
placeholder={placeholder}
/>
{search && (
<Button
<IconButton
variant="ghost"
size="small"
size="md"
aria-label={t(($) => $.clearSearch, {
ns: 'plugin',
label: accessibleLabel,
})}
onClick={handleClear}
className="size-6 min-h-0 shrink-0 p-0 focus-visible:ring-inset"
className="shrink-0 focus-visible:ring-inset"
>
<span className="i-ri-close-line size-4" aria-hidden />
</Button>
</IconButton>
)}
</div>
</>
@ -136,18 +136,18 @@ function SearchBox({
placeholder={placeholder}
/>
{search && (
<Button
<IconButton
variant="ghost"
size="small"
size="md"
aria-label={t(($) => $.clearSearch, {
ns: 'plugin',
label: accessibleLabel,
})}
onClick={handleClear}
className="size-6 min-h-0 shrink-0 p-0 focus-visible:ring-inset"
className="shrink-0 focus-visible:ring-inset"
>
<span className="i-ri-close-line size-4" aria-hidden />
</Button>
</IconButton>
)}
</div>
{showTags && (
@ -161,15 +161,15 @@ function SearchBox({
</div>
{supportAddCustomTool && (
<div className="flex shrink-0 items-center">
<Button
<IconButton
variant="primary"
size="small"
size="md"
aria-label={t(($) => $['addToolModal.custom.tip'], { ns: 'tools' })}
className="ml-2 size-6 min-h-0 rounded-full p-0"
className="ml-2 rounded-full"
onClick={onShowAddCustomCollectionModal}
>
<span className="i-ri-add-line size-4" aria-hidden />
</Button>
</IconButton>
</div>
)}
</div>

View File

@ -1,6 +1,7 @@
import type { Tag } from '../../../hooks'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { PopoverTrigger } from '@langgenius/dify-ui/popover'
import { memo, useEffect, useRef } from 'react'
import { useTranslation } from '#i18n'
@ -87,21 +88,21 @@ function MarketplaceTrigger({
}
/>
{!!selectedTagsLength && (
<Button
<IconButton
variant="ghost"
size="small"
size="md"
aria-label={t(($) => $.clearSelectedTags, {
ns: 'pluginTags',
tags: triggerLabel,
})}
className="absolute right-1 size-6 min-h-0 p-0 focus-visible:ring-inset"
className="absolute right-1 focus-visible:ring-inset"
onClick={() => {
shouldRestoreFocusRef.current = true
onTagsChange([])
}}
>
<span aria-hidden className="i-ri-close-circle-fill size-4 text-text-quaternary" />
</Button>
</IconButton>
)}
</div>
)

View File

@ -1,6 +1,7 @@
import type { Tag } from '../../../hooks'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { PopoverTrigger } from '@langgenius/dify-ui/popover'
import { memo, useEffect, useRef } from 'react'
import { useTranslation } from '#i18n'
@ -79,21 +80,21 @@ function ToolSelectorTrigger({
}
/>
{!!selectedTagsLength && (
<Button
<IconButton
variant="ghost"
size="small"
size="md"
aria-label={t(($) => $.clearSelectedTags, {
ns: 'pluginTags',
tags: triggerLabel,
})}
className="absolute right-0.5 size-6 min-h-0 p-0 focus-visible:ring-inset"
className="absolute right-0.5 focus-visible:ring-inset"
onClick={() => {
shouldRestoreFocusRef.current = true
onTagsChange([])
}}
>
<span aria-hidden className="i-ri-close-circle-fill size-4 text-text-quaternary" />
</Button>
</IconButton>
)}
</div>
)

View File

@ -18,7 +18,7 @@ import OAuthClientSettings from './oauth-client-settings'
export type AddOAuthButtonProps = {
pluginPayload: PluginPayload
buttonVariant?: ButtonProps['variant']
buttonVariant?: NonNullable<ButtonProps['variant']>
buttonText?: string
className?: string
buttonLeftClassName?: string
@ -218,11 +218,12 @@ const AddOAuthButton = ({
dividerClassName,
)}
></div>
<Button
<IconButton
variant={buttonVariant}
aria-label={t(($) => $['auth.oauthClientSettings'], { ns: 'plugin' })}
size="lg"
className={cn(
'size-8 shrink-0 rounded-l-none p-0 hover:bg-components-button-primary-bg-hover',
'shrink-0 rounded-l-none hover:bg-components-button-primary-bg-hover',
buttonRightClassName,
)}
disabled={disabled}
@ -231,7 +232,7 @@ const AddOAuthButton = ({
}}
>
<span className="i-ri-equalizer-2-line size-4" aria-hidden="true" />
</Button>
</IconButton>
</div>
)}
{!renderTrigger && !isConfigured && (

View File

@ -44,12 +44,6 @@ vi.mock('@/utils/var', () => ({
getMarketplaceUrl: (path: string) => `https://marketplace.example.com${path}`,
}))
vi.mock('@/app/components/base/action-button', () => ({
default: ({ children, ...props }: React.ButtonHTMLAttributes<HTMLButtonElement>) => (
<button {...props}>{children}</button>
),
}))
vi.mock('@/app/components/base/badge', () => ({
default: ({ text, children }: { text?: React.ReactNode; children?: React.ReactNode }) => (
<div data-testid="badge">{text ?? children}</div>

View File

@ -471,7 +471,7 @@ describe('MultipleToolSelector', () => {
const addSelector = screen.getByTestId('tool-selector-add')
expect(addSelector).toHaveAttribute('data-controlled-state', 'false')
// Act - Click add button (ActionButton)
// Act - Click the add icon button.
const actionButton = screen.getByRole('button', {
name: 'plugin.detailPanel.toolSelector.title',
})
@ -703,7 +703,7 @@ describe('MultipleToolSelector', () => {
// Arrange & Act
const { container } = renderComponent({ disabled: true })
// Assert - ActionButton (add button with mx-1 class) should not be rendered
// Assert - The add icon button should not be rendered.
const actionButton = container.querySelector('[class*="mx-1"]')
expect(actionButton).not.toBeInTheDocument()
})

View File

@ -1,9 +1,9 @@
import type { Node } from 'reactflow'
import type { ToolValue } from '@/app/components/workflow/block-selector/types'
import type { NodeOutPutVar } from '@/app/components/workflow/types'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from '@langgenius/dify-ui/collapsible'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import Divider from '@/app/components/base/divider'
@ -181,19 +181,19 @@ const MultipleToolSelector = ({
controlledState={selectorOpen}
onControlledStateChange={setSelectorOpen}
trigger={
<Button
<IconButton
ref={addToolButtonRef}
variant="ghost"
size="small"
size="md"
aria-label={t(($) => $['detailPanel.toolSelector.title'], { ns: 'plugin' })}
className="mx-1 size-6 min-h-0 p-0"
className="mx-1"
onClick={() => {
setToolsOpen(true)
setPanelShowState(true)
}}
>
<span className="i-ri-add-line size-4" aria-hidden />
</Button>
</IconButton>
}
panelShowState={panelShowState}
onPanelShowStateChange={setPanelShowState}

View File

@ -9,6 +9,7 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@langgenius/dify-ui/dropdown-menu'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
@ -71,14 +72,16 @@ export function OperationDropdown({
return (
<DropdownMenu>
<DropdownMenuTrigger
className={cn(
'action-btn data-popup-open:bg-state-base-hover',
triggerSize === 'xs' ? 'action-btn-xs' : 'action-btn-m',
)}
aria-label={t(($) => $['detailPanel.operation.moreActions'], { ns: 'plugin' })}
>
<span aria-hidden className="i-ri-more-fill size-4" />
</DropdownMenuTrigger>
render={
<IconButton
aria-label={t(($) => $['detailPanel.operation.moreActions'], { ns: 'plugin' })}
size={triggerSize === 'xs' ? 'xs' : 'md'}
className="data-popup-open:bg-state-base-hover"
>
<span aria-hidden className="i-ri-more-fill size-4" />
</IconButton>
}
/>
<DropdownMenuContent
placement={placement}
sideOffset={sideOffset}

View File

@ -1,6 +1,7 @@
'use client'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
import { StatusDot } from '@langgenius/dify-ui/status-dot'
import { Switch } from '@langgenius/dify-ui/switch'
@ -144,17 +145,17 @@ export function ToolItem({
</span>
)}
{onDelete && (
<Button
<IconButton
variant="ghost"
size="small"
size="md"
aria-label={t(($) => $['operation.delete'], { ns: 'common' })}
className="size-6 min-h-0 p-0 text-text-tertiary hover:text-text-destructive"
className="text-text-tertiary hover:text-text-destructive"
onClick={onDelete}
onMouseEnter={() => setIsDeleting(true)}
onMouseLeave={() => setIsDeleting(false)}
>
<span className="i-ri-delete-bin-line size-4" aria-hidden />
</Button>
</IconButton>
)}
</div>
{!isError &&

View File

@ -2,8 +2,6 @@
import type { ReactNode } from 'react'
import type { DocPathWithoutLang } from '@/types/doc-paths'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import {
DropdownMenu,
DropdownMenuContent,
@ -11,6 +9,7 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@langgenius/dify-ui/dropdown-menu'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { Fragment, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { MARKETPLACE_URL_PREFIX } from '@/config'
@ -86,19 +85,16 @@ export function SubmitRequestDropdown({ dividerAfterFirst }: SubmitRequestDropdo
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger
render={
<Button
<IconButton
aria-label={t(($) => $.requestSubmit, {
ns: 'plugin',
defaultValue: t(($) => $.requestAPlugin, { ns: 'plugin' }),
})}
variant="ghost"
className={cn(
'size-8 p-2 text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary',
'data-popup-open:bg-state-base-hover data-popup-open:text-text-secondary',
)}
size="lg"
className="data-popup-open:bg-state-base-hover data-popup-open:text-text-secondary"
>
<span className="i-ri-book-open-line size-4 shrink-0" />
</Button>
</IconButton>
}
/>
<DropdownMenuContent placement="bottom-end" sideOffset={4} popupClassName="min-w-[200px] p-1">

View File

@ -1,7 +1,7 @@
'use client'
import type { ReactNode } from 'react'
import { Button } from '@langgenius/dify-ui/button'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { PopoverClose, PopoverTitle } from '@langgenius/dify-ui/popover'
import { useTranslation } from 'react-i18next'
@ -26,13 +26,13 @@ export function PluginSidecarPanel({ children, footer, title }: PluginSidecarPan
</div>
<PopoverClose
render={
<Button
variant="ghost"
<IconButton
aria-label={t(($) => $['operation.close'], { ns: 'common' })}
className="absolute top-2.5 right-2.5 size-8 p-0 text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary"
size="lg"
className="absolute top-2.5 right-2.5"
>
<span aria-hidden className="i-ri-close-line size-4" />
</Button>
</IconButton>
}
/>
</div>

View File

@ -1,5 +1,6 @@
import type { PluginStatus } from '@/app/components/plugins/types'
import { fireEvent, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { PluginSource, TaskStatus } from '@/app/components/plugins/types'
// Import mocked modules
@ -67,8 +68,7 @@ const setupMocks = (plugins: PluginStatus[] = []) => {
return { mockMutateAsync, mockHandleRefetch }
}
const getTaskMenuTrigger = () =>
document.getElementById('plugin-task-trigger')!.closest('[role="button"]') as HTMLElement
const getTaskMenuTrigger = () => document.getElementById('plugin-task-trigger') as HTMLButtonElement
describe('usePluginTaskStatus Hook', () => {
beforeEach(() => {
@ -299,6 +299,7 @@ describe('usePluginTaskStatus Hook', () => {
// ============================================================================
describe('TaskStatusIndicator Component', () => {
const defaultProps = {
id: 'plugin-task-trigger',
tip: 'Test tooltip',
isInstalling: false,
isInstallingWithSuccess: false,
@ -307,7 +308,6 @@ describe('TaskStatusIndicator Component', () => {
isFailed: false,
successPluginsLength: 0,
runningPluginsLength: 0,
totalPluginsLength: 1,
onClick: vi.fn(),
}
@ -316,64 +316,29 @@ describe('TaskStatusIndicator Component', () => {
})
describe('Rendering', () => {
it('should render with correct id', () => {
it('should render an accessible button', () => {
render(<TaskStatusIndicator {...defaultProps} />)
expect(document.getElementById('plugin-task-trigger'))!.toBeInTheDocument()
})
})
describe('Icon display', () => {
it('should show downloading icon when installing', () => {
render(<TaskStatusIndicator {...defaultProps} isInstalling />)
// DownloadingIcon is rendered when isInstalling is true
// DownloadingIcon is rendered when isInstalling is true
expect(document.getElementById('plugin-task-trigger'))!.toBeInTheDocument()
})
it('should show downloading icon when installing with error', () => {
render(<TaskStatusIndicator {...defaultProps} isInstallingWithError />)
expect(document.getElementById('plugin-task-trigger'))!.toBeInTheDocument()
})
it('should show install icon when not installing', () => {
render(<TaskStatusIndicator {...defaultProps} isSuccess />)
expect(document.getElementById('plugin-task-trigger'))!.toBeInTheDocument()
expect(screen.getByRole('button', { name: defaultProps.tip })).toBeInTheDocument()
})
})
describe('Status badge', () => {
it('should not show a badge when installing has no success or error yet', () => {
render(
<TaskStatusIndicator
{...defaultProps}
isInstalling
successPluginsLength={1}
totalPluginsLength={3}
/>,
)
expect(document.getElementById('plugin-task-trigger'))!.toBeInTheDocument()
render(<TaskStatusIndicator {...defaultProps} isInstalling successPluginsLength={1} />)
expect(screen.queryByTestId('task-status-success-badge')).not.toBeInTheDocument()
expect(screen.queryByTestId('task-status-error-badge')).not.toBeInTheDocument()
})
it('should not show success badge when installing with success', () => {
render(
<TaskStatusIndicator
{...defaultProps}
isInstallingWithSuccess
successPluginsLength={2}
totalPluginsLength={3}
/>,
<TaskStatusIndicator {...defaultProps} isInstallingWithSuccess successPluginsLength={2} />,
)
expect(screen.queryByTestId('task-status-success-badge')).not.toBeInTheDocument()
})
it('should show error badge when installing with error', () => {
render(
<TaskStatusIndicator
{...defaultProps}
isInstallingWithError
runningPluginsLength={1}
totalPluginsLength={3}
/>,
<TaskStatusIndicator {...defaultProps} isInstallingWithError runningPluginsLength={1} />,
)
expect(screen.getByTestId('task-status-error-badge')).toBeInTheDocument()
})
@ -385,7 +350,6 @@ describe('TaskStatusIndicator Component', () => {
isSuccess
successPluginsLength={3}
runningPluginsLength={0}
totalPluginsLength={3}
/>,
)
expect(screen.getByTestId('task-status-success-badge')).toBeInTheDocument()
@ -397,20 +361,21 @@ describe('TaskStatusIndicator Component', () => {
})
})
describe('Styling', () => {
it('should apply cursor-pointer for statuses that open the task menu', () => {
describe('Availability', () => {
it('should keep active installation status enabled', () => {
render(<TaskStatusIndicator {...defaultProps} isInstalling />)
const trigger = document.getElementById('plugin-task-trigger')
expect(trigger)!.toHaveClass('cursor-pointer')
expect(trigger).toHaveAttribute('aria-disabled', 'false')
})
})
describe('User interactions', () => {
it('should call onClick when clicked', () => {
it('should call onClick when clicked', async () => {
const user = userEvent.setup()
const handleClick = vi.fn()
render(<TaskStatusIndicator {...defaultProps} onClick={handleClick} />)
fireEvent.click(document.getElementById('plugin-task-trigger')!)
await user.click(screen.getByRole('button', { name: defaultProps.tip }))
expect(handleClick).toHaveBeenCalledTimes(1)
})
@ -630,24 +595,27 @@ describe('PluginTasks Component', () => {
expect(screen.getByTestId('plugin-task-list'))!.toBeInTheDocument()
})
it('should apply open styling to the trigger while the task menu is expanded', () => {
it('should expose the expanded state on the trigger', async () => {
const user = userEvent.setup()
setupMocks([createMockPlugin({ status: TaskStatus.failed })])
render(<PluginTasks />)
fireEvent.click(getTaskMenuTrigger())
const taskMenuTrigger = getTaskMenuTrigger()
expect(taskMenuTrigger).not.toHaveAttribute('data-menu-open')
expect(document.getElementById('plugin-task-trigger'))!.toHaveClass(
'bg-state-destructive-hover-alt',
)
await user.click(taskMenuTrigger)
expect(taskMenuTrigger).toHaveAttribute('aria-expanded', 'true')
expect(taskMenuTrigger).toHaveAttribute('data-menu-open', '')
})
it('should apply pointer cursor to the task menu trigger when it can open', () => {
it('should keep the task menu trigger enabled when it can open', () => {
setupMocks([createMockPlugin({ status: TaskStatus.failed })])
render(<PluginTasks />)
expect(getTaskMenuTrigger()).toHaveClass('cursor-pointer')
expect(getTaskMenuTrigger()).toHaveAttribute('aria-disabled', 'false')
})
it('should apply custom dropdown positioning props', () => {

View File

@ -0,0 +1,22 @@
@reference "../../../../../styles/globals.css";
.indicator {
@apply relative overflow-visible bg-components-panel-bg p-2 shadow-none inset-ring-components-panel-border-subtle;
&[data-disabled] {
@apply cursor-default bg-components-panel-bg text-components-button-secondary-text shadow-none inset-ring-components-panel-border-subtle backdrop-blur-[5px];
}
&[data-menu-open] {
@apply bg-components-button-secondary-bg-hover shadow-xs shadow-shadow-shadow-3 inset-ring-components-button-secondary-border-hover backdrop-blur-[5px];
}
&[data-error] {
@apply cursor-pointer bg-state-destructive-hover text-components-button-destructive-secondary-text shadow-xs shadow-shadow-shadow-3 inset-ring-components-button-destructive-secondary-border-hover backdrop-blur-[5px];
&:hover,
&[data-menu-open] {
@apply bg-state-destructive-hover-alt;
}
}
}

View File

@ -1,21 +1,22 @@
import { Button } from '@langgenius/dify-ui/button'
import type { IconButtonProps } from '@langgenius/dify-ui/icon-button'
import { cn } from '@langgenius/dify-ui/cn'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import DownloadingIcon from '@/app/components/header/plugins-nav/downloading-icon'
import styles from './task-status-indicator.module.css'
type TaskStatusIndicatorProps = {
type TaskStatusIndicatorProps = Omit<
IconButtonProps,
'aria-label' | 'aria-labelledby' | 'children' | 'size' | 'tone' | 'variant'
> & {
tip: string
isInstalling: boolean
isInstallingWithSuccess: boolean
isInstallingWithError: boolean
isSuccess: boolean
isFailed: boolean
disabled?: boolean
isOpen?: boolean
successPluginsLength: number
runningPluginsLength: number
totalPluginsLength: number
onClick: () => void
}
function ErrorBadgeIcon() {
@ -63,11 +64,10 @@ function TaskStatusIndicator({
isInstallingWithError,
isSuccess,
isFailed,
disabled = false,
isOpen = false,
successPluginsLength,
runningPluginsLength,
onClick,
className,
...buttonProps
}: TaskStatusIndicatorProps) {
const showErrorStyle = isInstallingWithError || isFailed
const hasActiveInstall = isInstalling || isInstallingWithSuccess || isInstallingWithError
@ -76,42 +76,31 @@ function TaskStatusIndicator({
(!hasActiveInstall && !isFailed && successPluginsLength > 0 && runningPluginsLength === 0)
const showSuccessBadge = showSuccessIcon && !isInstallingWithError && !isFailed
const showBadge = isInstallingWithError || showSuccessBadge || isFailed
const isClickable = !disabled && (hasActiveInstall || isSuccess || isFailed)
return (
<Tooltip>
<TooltipTrigger
render={
<Button
type="button"
variant="secondary"
size="small"
disabled={disabled}
<IconButton
{...buttonProps}
aria-label={tip}
className={cn(
'relative size-8 overflow-visible rounded-lg bg-components-panel-bg p-2 shadow-none inset-ring-components-panel-border-subtle',
'focus-visible:ring-2 focus-visible:ring-state-accent-solid',
isClickable ? 'cursor-pointer' : 'cursor-default',
showErrorStyle &&
'cursor-pointer bg-state-destructive-hover text-components-button-destructive-secondary-text shadow-xs shadow-shadow-shadow-3 inset-ring-components-button-destructive-secondary-border-hover backdrop-blur-[5px] hover:bg-state-destructive-hover-alt',
isOpen &&
!showErrorStyle &&
'bg-components-button-secondary-bg-hover shadow-xs shadow-shadow-shadow-3 inset-ring-components-button-secondary-border-hover backdrop-blur-[5px]',
isOpen && showErrorStyle && 'bg-state-destructive-hover-alt',
)}
id="plugin-task-trigger"
onClick={onClick}
variant="secondary"
size="lg"
focusableWhenDisabled
data-error={showErrorStyle ? '' : undefined}
className={cn(styles.indicator, className)}
>
<DownloadingIcon active={hasActiveInstall} />
{showBadge && (
<div className="absolute -top-1.5 -right-1.5 box-content flex size-3.5 items-center justify-center rounded-full border border-components-panel-bg bg-components-panel-bg">
{isInstallingWithError && <ErrorBadgeIcon />}
{showSuccessBadge && <SuccessBadgeIcon />}
{isFailed && <ErrorBadgeIcon />}
</div>
)}
</Button>
<span className="contents">
<DownloadingIcon active={hasActiveInstall} />
{showBadge && (
<span className="absolute -top-1.5 -right-1.5 box-content flex size-3.5 items-center justify-center rounded-full border border-components-panel-bg bg-components-panel-bg">
{isInstallingWithError && <ErrorBadgeIcon />}
{showSuccessBadge && <SuccessBadgeIcon />}
{isFailed && <ErrorBadgeIcon />}
</span>
)}
</span>
</IconButton>
}
/>
<TooltipContent sideOffset={8}>{tip}</TooltipContent>

View File

@ -124,27 +124,20 @@ const PluginTasks = ({
<div className={rootClassName}>
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger
nativeButton={false}
render={(props, state) => (
<div
{...props}
className={cn('cursor-pointer data-disabled:cursor-default', props.className)}
>
<TaskStatusIndicator
tip={tip}
isInstalling={isInstalling}
isInstallingWithSuccess={isInstallingWithSuccess}
isInstallingWithError={isInstallingWithError}
isSuccess={isSuccess}
isFailed={isFailed}
isOpen={state.open}
successPluginsLength={successPluginsLength}
runningPluginsLength={runningPluginsLength}
totalPluginsLength={totalPluginsLength}
onClick={() => {}}
/>
</div>
)}
render={
<TaskStatusIndicator
id="plugin-task-trigger"
tip={tip}
isInstalling={isInstalling}
isInstallingWithSuccess={isInstallingWithSuccess}
isInstallingWithError={isInstallingWithError}
isSuccess={isSuccess}
isFailed={isFailed}
successPluginsLength={successPluginsLength}
runningPluginsLength={runningPluginsLength}
data-menu-open={open ? '' : undefined}
/>
}
disabled={!canOpenMenu}
/>
<DropdownMenuContent

View File

@ -1,8 +1,8 @@
import type { SearchParamsFromCollection } from '@dify/contracts/marketplace'
import type { ToolsContentInset } from '../content-inset'
import type { useMarketplace } from './hooks'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { useTheme } from 'next-themes'
import { useTranslation } from 'react-i18next'
import { useLocale } from '#i18n'
@ -53,15 +53,15 @@ const Marketplace = ({
<>
<div className="sticky bottom-0 flex shrink-0 flex-col bg-background-default-subtle pt-2 pb-3.5">
{isMarketplaceArrowVisible && (
<Button
<IconButton
aria-label={t(($) => $['marketplace.moreFrom'], { ns: 'plugin' })}
className="absolute top-2 left-1/2 z-10 size-6 -translate-x-1/2 p-0 text-text-quaternary"
className="absolute top-2 left-1/2 z-10 -translate-x-1/2 text-text-quaternary"
onClick={showMarketplacePanel}
size="small"
size="md"
variant="ghost"
>
<span aria-hidden="true" className="i-ri-arrow-up-double-line size-4" />
</Button>
</IconButton>
)}
<div className={cn('pt-4 pb-3', marketplaceFrameClassName)}>
<div className="bg-linear-to-r from-[rgba(11,165,236,0.95)] to-[rgba(21,90,239,0.95)] bg-clip-text title-2xl-semi-bold text-transparent">

View File

@ -25,7 +25,6 @@
@import './monaco-sticky-fix.css' layer(base);
/* Component CSS using @apply / @utility. */
@import '../components/base/action-button/index.css';
@import '../components/base/badge/index.css';
@import '../components/base/premium-badge/index.css';
@import './glass-surface.css';