mirror of
https://github.com/langgenius/dify.git
synced 2026-08-15 04:59:46 +08:00
refactor(web): migrate plugin icon commands (#40648)
This commit is contained in:
parent
288766171d
commit
d6f32a0267
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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()
|
||||
})
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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 &&
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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';
|
||||
|
||||
Loading…
Reference in New Issue
Block a user