mirror of
https://github.com/langgenius/dify.git
synced 2026-06-07 07:52:50 +08:00
refactor(web): use dropdown data attributes (#36431)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
cc9b90a5ae
commit
5a585c8618
@ -784,11 +784,6 @@
|
||||
"count": 10
|
||||
}
|
||||
},
|
||||
"web/app/components/base/chip/index.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"web/app/components/base/date-and-time-picker/hooks.ts": {
|
||||
"react/no-unnecessary-use-prefix": {
|
||||
"count": 2
|
||||
@ -1607,11 +1602,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/base/sort/index.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/base/svg-gallery/index.tsx": {
|
||||
"node/prefer-global/buffer": {
|
||||
"count": 1
|
||||
@ -2120,11 +2110,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/explore/item-operation/index.tsx": {
|
||||
"react/set-state-in-effect": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/explore/try-app/tab.tsx": {
|
||||
"erasable-syntax-only/enums": {
|
||||
"count": 1
|
||||
@ -2596,11 +2581,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/plugins/plugin-page/install-plugin-dropdown.tsx": {
|
||||
"react/set-state-in-effect": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/plugins/reference-setting-modal/auto-update-setting/types.ts": {
|
||||
"erasable-syntax-only/enums": {
|
||||
"count": 2
|
||||
@ -2778,11 +2758,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/share/text-generation/menu-dropdown.tsx": {
|
||||
"react/set-state-in-effect": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/share/text-generation/no-data/index.tsx": {
|
||||
"ts/no-empty-object-type": {
|
||||
"count": 1
|
||||
@ -2950,11 +2925,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/block-selector/market-place-plugin/action.tsx": {
|
||||
"react/set-state-in-effect": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/block-selector/market-place-plugin/item.tsx": {
|
||||
"erasable-syntax-only/enums": {
|
||||
"count": 1
|
||||
@ -4051,11 +4021,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/operator/zoom-in-out.tsx": {
|
||||
"erasable-syntax-only/enums": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/panel/__tests__/index.spec.tsx": {
|
||||
"react/static-components": {
|
||||
"count": 2
|
||||
@ -4378,11 +4343,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/workflow-preview/components/zoom-in-out.tsx": {
|
||||
"erasable-syntax-only/enums": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/education-apply/hooks.ts": {
|
||||
"react/set-state-in-effect": {
|
||||
"count": 5
|
||||
|
||||
@ -134,7 +134,7 @@ const DropDown = ({
|
||||
render={(
|
||||
<ActionButton
|
||||
aria-label={t('operation.more', { ns: 'common' })}
|
||||
className={cn(expand ? 'size-8 rounded-lg' : 'size-6 rounded-md', open && 'bg-state-base-hover')}
|
||||
className={cn(expand ? 'size-8 rounded-lg' : 'size-6 rounded-md', 'data-popup-open:bg-state-base-hover')}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
|
||||
@ -7,9 +7,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@langgenius/dify-ui/dropdown-menu'
|
||||
import { RiArrowDownSLine, RiCheckLine } from '@remixicon/react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import * as React from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type VersionSelectorProps = {
|
||||
@ -20,17 +18,7 @@ type VersionSelectorProps = {
|
||||
|
||||
const VersionSelector: React.FC<VersionSelectorProps> = ({ versionLen, value, onChange }) => {
|
||||
const { t } = useTranslation()
|
||||
const [isOpen, {
|
||||
setFalse: handleOpenFalse,
|
||||
set: handleOpenSet,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const moreThanOneVersion = versionLen > 1
|
||||
const handleOpen = useCallback((nextOpen: boolean) => {
|
||||
if (moreThanOneVersion)
|
||||
handleOpenSet(nextOpen)
|
||||
}, [moreThanOneVersion, handleOpenSet])
|
||||
|
||||
const versions = Array.from({ length: versionLen }, (_, index) => ({
|
||||
label: `${t('generate.version', { ns: 'appDebug' })} ${index + 1}${index === versionLen - 1 ? ` · ${t('generate.latest', { ns: 'appDebug' })}` : ''}`,
|
||||
value: index,
|
||||
@ -39,14 +27,12 @@ const VersionSelector: React.FC<VersionSelectorProps> = ({ versionLen, value, on
|
||||
const isLatest = value === versionLen - 1
|
||||
|
||||
return (
|
||||
<DropdownMenu
|
||||
open={isOpen}
|
||||
onOpenChange={handleOpen}
|
||||
>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
nativeButton={false}
|
||||
render={(
|
||||
<div className={cn('flex items-center system-xs-medium text-text-tertiary', isOpen && 'text-text-secondary', moreThanOneVersion && 'cursor-pointer')} />
|
||||
disabled={!moreThanOneVersion}
|
||||
className={cn(
|
||||
'flex items-center border-none bg-transparent p-0 system-xs-medium text-text-tertiary',
|
||||
moreThanOneVersion ? 'cursor-pointer data-popup-open:text-text-secondary' : 'cursor-default',
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
@ -63,14 +49,13 @@ const VersionSelector: React.FC<VersionSelectorProps> = ({ versionLen, value, on
|
||||
alignOffset={-12}
|
||||
popupClassName="w-[208px] rounded-xl border-[0.5px] bg-components-panel-bg-blur p-1"
|
||||
>
|
||||
<div className={cn('flex h-[22px] items-center px-3 pl-3 system-xs-medium-uppercase text-text-tertiary')}>
|
||||
<div className="flex h-[22px] items-center px-3 pl-3 system-xs-medium-uppercase text-text-tertiary">
|
||||
{t('generate.versions', { ns: 'appDebug' })}
|
||||
</div>
|
||||
<DropdownMenuRadioGroup
|
||||
value={value}
|
||||
onValueChange={(nextValue) => {
|
||||
onChange(nextValue)
|
||||
handleOpenFalse()
|
||||
}}
|
||||
>
|
||||
{versions.map(option => (
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import type { CSSProperties, FC } from 'react'
|
||||
import type { ModelAndParameter } from '../types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@ -8,7 +7,7 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@langgenius/dify-ui/dropdown-menu'
|
||||
import { memo, useState } from 'react'
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import { ModelStatusEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
@ -42,10 +41,8 @@ const DebugItem: FC<DebugItemProps> = ({
|
||||
const index = multipleModelConfigs.findIndex(v => v.id === modelAndParameter.id)
|
||||
const currentProvider = textGenerationModelList.find(item => item.provider === modelAndParameter.provider)
|
||||
const currentModel = currentProvider?.models.find(item => item.model === modelAndParameter.model)
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const handleDuplicate = () => {
|
||||
setOpen(false)
|
||||
if (multipleModelConfigs.length >= 4)
|
||||
return
|
||||
|
||||
@ -63,12 +60,10 @@ const DebugItem: FC<DebugItemProps> = ({
|
||||
}
|
||||
|
||||
const handleDebugAsSingleModel = () => {
|
||||
setOpen(false)
|
||||
onDebugWithMultipleModelChange(modelAndParameter)
|
||||
}
|
||||
|
||||
const handleRemove = () => {
|
||||
setOpen(false)
|
||||
onMultipleModelConfigsChange(
|
||||
true,
|
||||
multipleModelConfigs.filter(item => item.id !== modelAndParameter.id),
|
||||
@ -92,11 +87,11 @@ const DebugItem: FC<DebugItemProps> = ({
|
||||
<ModelParameterTrigger
|
||||
modelAndParameter={modelAndParameter}
|
||||
/>
|
||||
<DropdownMenu open={open} onOpenChange={setOpen}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={(
|
||||
<ActionButton
|
||||
className={cn(open && 'bg-state-base-hover', 'focus-visible:ring-2 focus-visible:ring-state-accent-solid')}
|
||||
className="focus-visible:ring-2 focus-visible:ring-state-accent-solid data-popup-open:bg-state-base-hover"
|
||||
aria-label={t('operation.more', { ns: 'common' })}
|
||||
>
|
||||
<span aria-hidden className="i-ri-more-fill size-4 text-text-tertiary" />
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
import type { QueryParam } from '../index'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { useState } from 'react'
|
||||
import Filter, { TIME_PERIOD_MAPPING } from '../filter'
|
||||
@ -297,13 +297,13 @@ describe('Filter', () => {
|
||||
|
||||
it('should call setQueryParams when typing in search', async () => {
|
||||
const user = userEvent.setup()
|
||||
const setQueryParams = vi.fn()
|
||||
const onSetQueryParams = vi.fn()
|
||||
|
||||
const Wrapper = () => {
|
||||
const [queryParams, updateQueryParams] = useState<QueryParam>(createDefaultQueryParams())
|
||||
const [queryParams, setQueryParams] = useState<QueryParam>(() => createDefaultQueryParams())
|
||||
const handleSetQueryParams = (next: QueryParam) => {
|
||||
updateQueryParams(next)
|
||||
setQueryParams(next)
|
||||
onSetQueryParams(next)
|
||||
}
|
||||
return (
|
||||
<Filter
|
||||
@ -319,7 +319,7 @@ describe('Filter', () => {
|
||||
await user.type(input, 'workflow')
|
||||
|
||||
// Should call setQueryParams for each character typed
|
||||
expect(setQueryParams).toHaveBeenLastCalledWith(
|
||||
expect(onSetQueryParams).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ keyword: 'workflow' }),
|
||||
)
|
||||
})
|
||||
@ -335,7 +335,9 @@ describe('Filter', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.clear' }))
|
||||
const searchInput = screen.getByPlaceholderText('common.operation.search')
|
||||
const searchField = searchInput.closest('div')!
|
||||
await user.click(within(searchField).getByRole('button', { name: 'common.operation.clear' }))
|
||||
|
||||
expect(setQueryParams).toHaveBeenCalledWith({
|
||||
status: 'all',
|
||||
|
||||
@ -532,8 +532,7 @@ const AppCard = ({ app, onlineUsers = [], onRefresh, onOpenTagManagement = () =>
|
||||
<DropdownMenuTrigger
|
||||
aria-label={t('operation.more', { ns: 'common' })}
|
||||
className={cn(
|
||||
isOperationsMenuOpen ? 'bg-state-base-hover shadow-none' : 'bg-transparent',
|
||||
'flex size-8 items-center justify-center rounded-md border-none p-2 hover:bg-state-base-hover focus-visible:bg-state-base-hover focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:ring-inset',
|
||||
'flex size-8 items-center justify-center rounded-md border-none bg-transparent p-2 hover:bg-state-base-hover focus-visible:bg-state-base-hover focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:ring-inset data-popup-open:bg-state-base-hover data-popup-open:shadow-none',
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
|
||||
@ -95,10 +95,11 @@ describe('Operation Component', () => {
|
||||
const trigger = screen.getByText('Chat Title').closest('.cursor-pointer')
|
||||
|
||||
// closed state
|
||||
expect(trigger).not.toHaveClass('bg-state-base-hover')
|
||||
expect(trigger).toHaveClass('data-popup-open:bg-state-base-hover')
|
||||
expect(trigger).not.toHaveAttribute('data-popup-open')
|
||||
|
||||
// open state
|
||||
await user.click(screen.getByText('Chat Title'))
|
||||
expect(trigger).toHaveClass('bg-state-base-hover')
|
||||
expect(trigger).toHaveAttribute('data-popup-open')
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
'use client'
|
||||
import type { Placement } from '@langgenius/dify-ui/dropdown-menu'
|
||||
import type { FC } from 'react'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@ -9,7 +8,6 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@langgenius/dify-ui/dropdown-menu'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type Props = {
|
||||
@ -23,6 +21,10 @@ type Props = {
|
||||
placement?: Placement
|
||||
}
|
||||
|
||||
const deferAction = (action: () => void) => {
|
||||
queueMicrotask(action)
|
||||
}
|
||||
|
||||
const Operation: FC<Props> = ({
|
||||
title,
|
||||
isPinned,
|
||||
@ -34,22 +36,11 @@ const Operation: FC<Props> = ({
|
||||
placement = 'bottom-start',
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
const handleDeferredAction = useCallback((action: () => void) => {
|
||||
setOpen(false)
|
||||
queueMicrotask(action)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<DropdownMenu
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
className={cn(
|
||||
'flex cursor-pointer items-center rounded-lg border-none bg-transparent p-1.5 pl-2 text-text-secondary outline-hidden hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid',
|
||||
open && 'bg-state-base-hover',
|
||||
)}
|
||||
className="flex cursor-pointer items-center rounded-lg border-none bg-transparent p-1.5 pl-2 text-text-secondary outline-hidden hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid data-popup-open:bg-state-base-hover"
|
||||
>
|
||||
<span className="system-md-semibold">{title}</span>
|
||||
<span aria-hidden className="i-ri-arrow-down-s-line size-4" />
|
||||
@ -65,7 +56,7 @@ const Operation: FC<Props> = ({
|
||||
{isShowRenameConversation && (
|
||||
<DropdownMenuItem
|
||||
className="system-md-regular"
|
||||
onClick={() => onRenameConversation && handleDeferredAction(onRenameConversation)}
|
||||
onClick={() => onRenameConversation && deferAction(onRenameConversation)}
|
||||
>
|
||||
<span className="grow">{t('sidebar.action.rename', { ns: 'explore' })}</span>
|
||||
</DropdownMenuItem>
|
||||
@ -74,7 +65,7 @@ const Operation: FC<Props> = ({
|
||||
<DropdownMenuItem
|
||||
variant="destructive"
|
||||
className="system-md-regular"
|
||||
onClick={() => handleDeferredAction(onDelete)}
|
||||
onClick={() => deferAction(onDelete)}
|
||||
>
|
||||
<span className="grow">{t('sidebar.action.delete', { ns: 'explore' })}</span>
|
||||
</DropdownMenuItem>
|
||||
|
||||
@ -10,11 +10,6 @@ import {
|
||||
} from '@langgenius/dify-ui/alert-dialog'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
RiEditBoxLine,
|
||||
RiExpandRightLine,
|
||||
RiLayoutLeft2Line,
|
||||
} from '@remixicon/react'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import {
|
||||
useCallback,
|
||||
@ -35,7 +30,7 @@ type Props = {
|
||||
panelVisible?: boolean
|
||||
}
|
||||
|
||||
const Sidebar = ({ isPanel, panelVisible }: Props) => {
|
||||
const Sidebar = ({ isPanel }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
isInstalledApp,
|
||||
@ -112,18 +107,18 @@ const Sidebar = ({ isPanel, panelVisible }: Props) => {
|
||||
<div className={cn('grow truncate system-md-semibold text-text-secondary')}>{appData?.site.title}</div>
|
||||
{!isMobile && isSidebarCollapsed && (
|
||||
<ActionButton size="l" onClick={() => handleSidebarCollapse(false)}>
|
||||
<RiExpandRightLine className="h-[18px] w-[18px]" />
|
||||
<span aria-hidden className="i-ri-expand-right-line h-[18px] w-[18px]" />
|
||||
</ActionButton>
|
||||
)}
|
||||
{!isMobile && !isSidebarCollapsed && (
|
||||
<ActionButton size="l" onClick={() => handleSidebarCollapse(true)}>
|
||||
<RiLayoutLeft2Line className="h-[18px] w-[18px]" />
|
||||
<span aria-hidden className="i-ri-layout-left-2-line h-[18px] w-[18px]" />
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>
|
||||
<div className="shrink-0 px-3 py-4">
|
||||
<Button variant="secondary-accent" disabled={isResponding} className="w-full justify-center" onClick={handleNewConversation}>
|
||||
<RiEditBoxLine className="mr-1 size-4" />
|
||||
<span aria-hidden className="mr-1 i-ri-edit-box-line size-4" />
|
||||
{t('chat.newChat', { ns: 'share' })}
|
||||
</Button>
|
||||
</div>
|
||||
@ -156,7 +151,6 @@ const Sidebar = ({ isPanel, panelVisible }: Props) => {
|
||||
hideLogout={isInstalledApp}
|
||||
placement="top-start"
|
||||
data={appData?.site}
|
||||
forceClose={isPanel && !panelVisible}
|
||||
/>
|
||||
{/* powered by */}
|
||||
<div className="shrink-0">
|
||||
|
||||
@ -40,7 +40,7 @@ describe('Chip', () => {
|
||||
|
||||
// Helper function to get the trigger element
|
||||
const getTrigger = (container: HTMLElement) => {
|
||||
return container.querySelector('[role="button"][aria-haspopup="menu"]') as HTMLElement | null
|
||||
return container.querySelector('button[aria-haspopup="menu"], [role="button"][aria-haspopup="menu"]') as HTMLElement | null
|
||||
}
|
||||
|
||||
// Helper function to open dropdown panel
|
||||
@ -98,12 +98,11 @@ describe('Chip', () => {
|
||||
})
|
||||
|
||||
it('should hide left icon when showLeftIcon is false', () => {
|
||||
renderChip({ showLeftIcon: false })
|
||||
renderChip({ showLeftIcon: false, value: '' })
|
||||
|
||||
// When showLeftIcon is false, there should be no filter icon before the text
|
||||
const textElement = screen.getByText('All Items')
|
||||
const parent = textElement.closest('[role="button"]')
|
||||
const icons = parent?.querySelectorAll('svg')
|
||||
const trigger = getTrigger(document.body)
|
||||
const icons = trigger?.querySelectorAll('svg')
|
||||
|
||||
// Should only have the arrow icon, not the filter icon
|
||||
expect(icons?.length).toBe(1)
|
||||
@ -190,12 +189,7 @@ describe('Chip', () => {
|
||||
it('should call onClear when clear button is clicked', () => {
|
||||
const { container } = renderChip({ value: 'active' })
|
||||
|
||||
// Find the close icon (last SVG in the trigger) and click its parent
|
||||
const trigger = getTrigger(container)
|
||||
const svgs = trigger?.querySelectorAll('svg')
|
||||
// The close icon should be the last SVG element
|
||||
const closeIcon = svgs?.[svgs.length - 1]
|
||||
const clearButton = closeIcon?.parentElement
|
||||
const clearButton = container.querySelector('button[aria-label="common.operation.clear"]')
|
||||
|
||||
expect(clearButton)!.toBeInTheDocument()
|
||||
if (clearButton)
|
||||
@ -210,10 +204,7 @@ describe('Chip', () => {
|
||||
const trigger = getTrigger(container)
|
||||
expect(trigger)!.toHaveAttribute('aria-expanded', 'false')
|
||||
|
||||
// Find the close icon (last SVG) and click its parent
|
||||
const svgs = trigger?.querySelectorAll('svg')
|
||||
const closeIcon = svgs?.[svgs.length - 1]
|
||||
const clearButton = closeIcon?.parentElement
|
||||
const clearButton = container.querySelector('button[aria-label="common.operation.clear"]')
|
||||
|
||||
if (clearButton)
|
||||
fireEvent.click(clearButton)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { FC } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
DropdownMenu,
|
||||
@ -8,24 +8,28 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@langgenius/dify-ui/dropdown-menu'
|
||||
import { RiArrowDownSLine, RiCheckLine, RiCloseCircleFill, RiFilter3Line } from '@remixicon/react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export type Item = {
|
||||
value: number | string
|
||||
type ItemValue = number | string
|
||||
|
||||
export type Item<T extends ItemValue = ItemValue> = {
|
||||
value: T
|
||||
name: string
|
||||
} & Record<string, any>
|
||||
} & Record<string, unknown>
|
||||
|
||||
type Props = {
|
||||
type Props<T extends ItemValue> = {
|
||||
className?: string
|
||||
panelClassName?: string
|
||||
showLeftIcon?: boolean
|
||||
leftIcon?: any
|
||||
value: number | string
|
||||
items: Item[]
|
||||
onSelect: (item: any) => void
|
||||
leftIcon?: ReactNode
|
||||
value: T
|
||||
items: Item<T>[]
|
||||
onSelect: (item: Item<T>) => void
|
||||
onClear: () => void
|
||||
}
|
||||
const Chip: FC<Props> = ({
|
||||
|
||||
function Chip<T extends ItemValue>({
|
||||
className,
|
||||
panelClassName,
|
||||
showLeftIcon = true,
|
||||
@ -34,31 +38,24 @@ const Chip: FC<Props> = ({
|
||||
items,
|
||||
onSelect,
|
||||
onClear,
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
}: Props<T>) {
|
||||
const { t } = useTranslation()
|
||||
const triggerContent = useMemo(() => {
|
||||
return items.find(item => item.value === value)?.name || ''
|
||||
}, [items, value])
|
||||
|
||||
return (
|
||||
<DropdownMenu
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
>
|
||||
<DropdownMenu>
|
||||
<div className="relative">
|
||||
<DropdownMenuTrigger
|
||||
nativeButton={false}
|
||||
render={<div className="block" />}
|
||||
>
|
||||
<div className={cn(
|
||||
<div
|
||||
className={cn(
|
||||
'flex min-h-8 cursor-pointer items-center rounded-lg border-[0.5px] border-transparent bg-components-input-bg-normal px-2 py-1 hover:bg-state-base-hover-alt',
|
||||
open && !value && 'bg-state-base-hover-alt! hover:bg-state-base-hover-alt',
|
||||
!open && !!value && 'border-components-button-secondary-border! bg-components-button-secondary-bg! shadow-xs hover:border-components-button-secondary-border-hover hover:bg-components-button-secondary-bg-hover!',
|
||||
open && !!value && 'border-components-button-secondary-border-hover! bg-components-button-secondary-bg-hover! shadow-xs hover:border-components-button-secondary-border-hover hover:bg-components-button-secondary-bg-hover!',
|
||||
!value && 'has-data-popup-open:bg-state-base-hover-alt! has-data-popup-open:hover:bg-state-base-hover-alt',
|
||||
!!value && 'border-components-button-secondary-border! bg-components-button-secondary-bg! shadow-xs hover:border-components-button-secondary-border-hover hover:bg-components-button-secondary-bg-hover! has-data-popup-open:border-components-button-secondary-border-hover! has-data-popup-open:bg-components-button-secondary-bg-hover! has-data-popup-open:hover:border-components-button-secondary-border-hover has-data-popup-open:hover:bg-components-button-secondary-bg-hover!',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
>
|
||||
<DropdownMenuTrigger className="flex min-w-0 grow items-center border-none bg-transparent p-0 text-left">
|
||||
{showLeftIcon && (
|
||||
<div className="p-0.5">
|
||||
{leftIcon || (
|
||||
@ -72,19 +69,21 @@ const Chip: FC<Props> = ({
|
||||
</div>
|
||||
</div>
|
||||
{!value && <RiArrowDownSLine className="size-4 text-text-tertiary" />}
|
||||
{!!value && (
|
||||
<div
|
||||
className="group/clear cursor-pointer p-px"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onClear()
|
||||
}}
|
||||
>
|
||||
<RiCloseCircleFill className="size-3.5 text-text-quaternary group-hover/clear:text-text-tertiary" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
</DropdownMenuTrigger>
|
||||
{!!value && (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('operation.clear', { ns: 'common' })}
|
||||
className="group/clear cursor-pointer border-none bg-transparent p-px"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onClear()
|
||||
}}
|
||||
>
|
||||
<RiCloseCircleFill className="size-3.5 text-text-quaternary group-hover/clear:text-text-tertiary" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<DropdownMenuContent
|
||||
placement="bottom-start"
|
||||
sideOffset={4}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import type { FC } from 'react'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
DropdownMenu,
|
||||
@ -8,28 +7,28 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@langgenius/dify-ui/dropdown-menu'
|
||||
import { RiArrowDownSLine, RiCheckLine, RiSortAsc, RiSortDesc } from '@remixicon/react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type Item = {
|
||||
value: number | string
|
||||
name: string
|
||||
} & Record<string, any>
|
||||
} & Record<string, unknown>
|
||||
|
||||
type Props = {
|
||||
order?: string
|
||||
value: number | string
|
||||
items: Item[]
|
||||
onSelect: (item: any) => void
|
||||
onSelect: (value: string) => void
|
||||
}
|
||||
const Sort: FC<Props> = ({
|
||||
|
||||
function Sort({
|
||||
order,
|
||||
value,
|
||||
items,
|
||||
onSelect,
|
||||
}) => {
|
||||
}: Props) {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const triggerContent = useMemo(() => {
|
||||
return items.find(item => item.value === value)?.name || ''
|
||||
@ -37,28 +36,18 @@ const Sort: FC<Props> = ({
|
||||
|
||||
return (
|
||||
<div className="inline-flex items-center">
|
||||
<DropdownMenu
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
>
|
||||
<DropdownMenu>
|
||||
<div className="relative">
|
||||
<DropdownMenuTrigger
|
||||
nativeButton={false}
|
||||
render={<div className="block" />}
|
||||
className="flex min-h-8 cursor-pointer items-center rounded-l-lg border-none bg-components-input-bg-normal px-2 py-1 hover:bg-state-base-hover-alt data-popup-open:bg-state-base-hover-alt! data-popup-open:hover:bg-state-base-hover-alt"
|
||||
>
|
||||
<div className={cn(
|
||||
'flex min-h-8 cursor-pointer items-center rounded-l-lg bg-components-input-bg-normal px-2 py-1 hover:bg-state-base-hover-alt',
|
||||
open && 'bg-state-base-hover-alt! hover:bg-state-base-hover-alt',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-0.5 px-1">
|
||||
<div className="system-sm-regular text-text-tertiary">{t('filter.sortBy', { ns: 'appLog' })}</div>
|
||||
<div className={cn('system-sm-regular text-text-tertiary', !!value && 'text-text-secondary')}>
|
||||
{triggerContent}
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5 px-1">
|
||||
<div className="system-sm-regular text-text-tertiary">{t('filter.sortBy', { ns: 'appLog' })}</div>
|
||||
<div className={cn('system-sm-regular text-text-tertiary', !!value && 'text-text-secondary')}>
|
||||
{triggerContent}
|
||||
</div>
|
||||
<RiArrowDownSLine className="size-4 text-text-tertiary" />
|
||||
</div>
|
||||
<RiArrowDownSLine className="size-4 text-text-tertiary" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
placement="bottom-start"
|
||||
|
||||
@ -50,7 +50,7 @@ const PreviewDocumentPicker: FC<Props> = ({
|
||||
<PopoverTrigger
|
||||
nativeButton={false}
|
||||
render={(
|
||||
<div className={cn('flex h-6 items-center rounded-md px-1 select-none hover:bg-state-base-hover', open && 'bg-state-base-hover', className)}>
|
||||
<div className={cn('flex h-6 items-center rounded-md px-1 select-none hover:bg-state-base-hover data-popup-open:bg-state-base-hover', className)}>
|
||||
<FileIcon name={name} extension={extension} size="lg" />
|
||||
<div className="ml-1 flex flex-col items-start">
|
||||
<div className="flex items-center space-x-0.5">
|
||||
|
||||
@ -59,7 +59,7 @@ const Actions = ({
|
||||
aria-label={t('operation.more', { ns: 'common' })}
|
||||
className={cn(
|
||||
'flex size-8 cursor-pointer items-center justify-center rounded-lg p-0 shadow-xs shadow-shadow-shadow-3',
|
||||
isMoreOperationsOpen && 'bg-state-base-hover',
|
||||
'data-popup-open:bg-state-base-hover',
|
||||
)}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
|
||||
@ -368,9 +368,9 @@ describe('Dropdown', () => {
|
||||
// Act - Open dropdown
|
||||
fireEvent.click(button)
|
||||
|
||||
// Assert - Open state: should have bg-state-base-hover
|
||||
// Assert - Open state is exposed declaratively via data-popup-open
|
||||
await waitFor(() => {
|
||||
expect(button)!.toHaveClass('bg-state-base-hover')
|
||||
expect(button).toHaveAttribute('data-popup-open')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,7 +1,19 @@
|
||||
import type { ReactElement } from 'react'
|
||||
import { DropdownMenu, DropdownMenuContent } from '@langgenius/dify-ui/dropdown-menu'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import Item from '../item'
|
||||
|
||||
const renderItem = (ui: ReactElement) => {
|
||||
return render(
|
||||
<DropdownMenu open>
|
||||
<DropdownMenuContent>
|
||||
{ui}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>,
|
||||
)
|
||||
}
|
||||
|
||||
describe('Item', () => {
|
||||
const defaultProps = {
|
||||
name: 'Documents',
|
||||
@ -16,7 +28,7 @@ describe('Item', () => {
|
||||
// Rendering: verify the breadcrumb name is displayed
|
||||
describe('Rendering', () => {
|
||||
it('should render breadcrumb name', () => {
|
||||
render(<Item {...defaultProps} />)
|
||||
renderItem(<Item {...defaultProps} />)
|
||||
|
||||
expect(screen.getByText('Documents')).toBeInTheDocument()
|
||||
})
|
||||
@ -25,7 +37,7 @@ describe('Item', () => {
|
||||
// User interactions: clicking triggers callback with correct index
|
||||
describe('User Interactions', () => {
|
||||
it('should call onBreadcrumbClick with correct index on click', () => {
|
||||
render(<Item {...defaultProps} />)
|
||||
renderItem(<Item {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByText('Documents'))
|
||||
|
||||
@ -34,7 +46,7 @@ describe('Item', () => {
|
||||
})
|
||||
|
||||
it('should pass different index values correctly', () => {
|
||||
render(<Item {...defaultProps} index={5} />)
|
||||
renderItem(<Item {...defaultProps} index={5} />)
|
||||
|
||||
fireEvent.click(screen.getByText('Documents'))
|
||||
|
||||
|
||||
@ -1,7 +1,19 @@
|
||||
import type { ReactElement } from 'react'
|
||||
import { DropdownMenu, DropdownMenuContent } from '@langgenius/dify-ui/dropdown-menu'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import Menu from '../menu'
|
||||
|
||||
const renderMenu = (ui: ReactElement) => {
|
||||
return render(
|
||||
<DropdownMenu open>
|
||||
<DropdownMenuContent>
|
||||
{ui}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>,
|
||||
)
|
||||
}
|
||||
|
||||
describe('Menu', () => {
|
||||
const defaultProps = {
|
||||
breadcrumbs: ['Folder A', 'Folder B', 'Folder C'],
|
||||
@ -16,7 +28,7 @@ describe('Menu', () => {
|
||||
// Rendering: verify all breadcrumb items are displayed
|
||||
describe('Rendering', () => {
|
||||
it('should render all breadcrumb items', () => {
|
||||
render(<Menu {...defaultProps} />)
|
||||
renderMenu(<Menu {...defaultProps} />)
|
||||
|
||||
expect(screen.getByText('Folder A')).toBeInTheDocument()
|
||||
expect(screen.getByText('Folder B')).toBeInTheDocument()
|
||||
@ -36,7 +48,7 @@ describe('Menu', () => {
|
||||
// Index mapping: startIndex offsets are applied correctly
|
||||
describe('Index Mapping', () => {
|
||||
it('should pass correct index (startIndex + offset) to each item', () => {
|
||||
render(<Menu {...defaultProps} />)
|
||||
renderMenu(<Menu {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByText('Folder A'))
|
||||
expect(defaultProps.onBreadcrumbClick).toHaveBeenCalledWith(1)
|
||||
@ -49,7 +61,7 @@ describe('Menu', () => {
|
||||
})
|
||||
|
||||
it('should offset from startIndex of zero', () => {
|
||||
render(
|
||||
renderMenu(
|
||||
<Menu
|
||||
breadcrumbs={['First', 'Second']}
|
||||
startIndex={0}
|
||||
@ -68,7 +80,7 @@ describe('Menu', () => {
|
||||
// User interactions: clicking items triggers the callback
|
||||
describe('User Interactions', () => {
|
||||
it('should call onBreadcrumbClick with correct index when item clicked', () => {
|
||||
render(<Menu {...defaultProps} />)
|
||||
renderMenu(<Menu {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByText('Folder B'))
|
||||
|
||||
|
||||
@ -5,7 +5,6 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@langgenius/dify-ui/dropdown-menu'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Menu from './menu'
|
||||
|
||||
@ -21,18 +20,9 @@ const Dropdown = ({
|
||||
onBreadcrumbClick,
|
||||
}: DropdownProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const handleBreadCrumbClick = useCallback((index: number) => {
|
||||
onBreadcrumbClick(index)
|
||||
setOpen(false)
|
||||
}, [onBreadcrumbClick])
|
||||
|
||||
return (
|
||||
<DropdownMenu
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={(
|
||||
<button
|
||||
@ -40,8 +30,7 @@ const Dropdown = ({
|
||||
aria-label={t('operation.more', { ns: 'common' })}
|
||||
className={cn(
|
||||
'flex size-6 items-center justify-center rounded-md',
|
||||
'focus-visible:ring-2 focus-visible:ring-state-accent-solid',
|
||||
open ? 'bg-state-base-hover' : 'hover:bg-state-base-hover',
|
||||
'hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid data-popup-open:bg-state-base-hover',
|
||||
)}
|
||||
>
|
||||
<span aria-hidden className="i-ri-more-fill size-4 text-text-tertiary" />
|
||||
@ -56,7 +45,7 @@ const Dropdown = ({
|
||||
<Menu
|
||||
breadcrumbs={breadcrumbs}
|
||||
startIndex={startIndex}
|
||||
onBreadcrumbClick={handleBreadCrumbClick}
|
||||
onBreadcrumbClick={onBreadcrumbClick}
|
||||
/>
|
||||
</DropdownMenuContent>
|
||||
<span className="system-xs-regular text-divider-deep">/</span>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { DropdownMenuItem } from '@langgenius/dify-ui/dropdown-menu'
|
||||
import * as React from 'react'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
type ItemProps = {
|
||||
name: string
|
||||
@ -12,17 +12,13 @@ const Item = ({
|
||||
index,
|
||||
onBreadcrumbClick,
|
||||
}: ItemProps) => {
|
||||
const handleClick = useCallback(() => {
|
||||
onBreadcrumbClick(index)
|
||||
}, [index, onBreadcrumbClick])
|
||||
|
||||
return (
|
||||
<div
|
||||
<DropdownMenuItem
|
||||
className="rounded-lg px-3 py-1.5 system-md-regular text-text-secondary hover:bg-state-base-hover"
|
||||
onClick={handleClick}
|
||||
onClick={() => onBreadcrumbClick(index)}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,93 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import StatusItem from '../status-item'
|
||||
|
||||
describe('StatusItem', () => {
|
||||
const defaultItem = {
|
||||
value: '1',
|
||||
name: 'Test Status',
|
||||
}
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
const { container } = render(<StatusItem item={defaultItem} selected={false} />)
|
||||
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render item name', () => {
|
||||
render(<StatusItem item={defaultItem} selected={false} />)
|
||||
|
||||
expect(screen.getByText('Test Status')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render with correct styling classes', () => {
|
||||
const { container } = render(<StatusItem item={defaultItem} selected={false} />)
|
||||
|
||||
const wrapper = container.firstChild as HTMLElement
|
||||
expect(wrapper).toHaveClass('flex')
|
||||
expect(wrapper).toHaveClass('items-center')
|
||||
expect(wrapper).toHaveClass('justify-between')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Props', () => {
|
||||
it('should show check icon when selected is true', () => {
|
||||
const { container } = render(<StatusItem item={defaultItem} selected={true} />)
|
||||
|
||||
// Assert - RiCheckLine icon should be present
|
||||
const checkIcon = container.querySelector('.text-text-accent')
|
||||
expect(checkIcon).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not show check icon when selected is false', () => {
|
||||
const { container } = render(<StatusItem item={defaultItem} selected={false} />)
|
||||
|
||||
// Assert - RiCheckLine icon should not be present
|
||||
const checkIcon = container.querySelector('.text-text-accent')
|
||||
expect(checkIcon).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render different item names', () => {
|
||||
const item = { value: '2', name: 'Different Status' }
|
||||
render(<StatusItem item={item} selected={false} />)
|
||||
|
||||
expect(screen.getByText('Different Status')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Memoization', () => {
|
||||
it('should render consistently with same props', () => {
|
||||
const { container: container1 } = render(<StatusItem item={defaultItem} selected={true} />)
|
||||
const { container: container2 } = render(<StatusItem item={defaultItem} selected={true} />)
|
||||
|
||||
expect(container1.textContent).toBe(container2.textContent)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty item name', () => {
|
||||
const emptyItem = { value: '1', name: '' }
|
||||
|
||||
const { container } = render(<StatusItem item={emptyItem} selected={false} />)
|
||||
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle special characters in item name', () => {
|
||||
const specialItem = { value: '1', name: 'Status <>&"' }
|
||||
|
||||
render(<StatusItem item={specialItem} selected={false} />)
|
||||
|
||||
expect(screen.getByText('Status <>&"')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should maintain structure when rerendered', () => {
|
||||
const { rerender } = render(<StatusItem item={defaultItem} selected={false} />)
|
||||
|
||||
rerender(<StatusItem item={defaultItem} selected={true} />)
|
||||
|
||||
expect(screen.getByText('Test Status')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -11,10 +11,6 @@ vi.mock('../../display-toggle', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../../status-item', () => ({
|
||||
default: ({ item }: { item: { name: string } }) => <div data-testid="status-item">{item.name}</div>,
|
||||
}))
|
||||
|
||||
describe('MenuBar', () => {
|
||||
const defaultProps = {
|
||||
hasSelectableSegments: true,
|
||||
@ -87,22 +83,19 @@ describe('MenuBar', () => {
|
||||
expect(defaultProps.onInputChange).toHaveBeenCalledWith('')
|
||||
})
|
||||
|
||||
it('should render select with status items via renderOption', () => {
|
||||
it('should render the selected status in the trigger', () => {
|
||||
renderMenuBar()
|
||||
expect(screen.getByText('All')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call renderOption for each item when dropdown is opened', async () => {
|
||||
it('should render status options when dropdown is opened', async () => {
|
||||
renderMenuBar()
|
||||
|
||||
const selectButton = screen.getByRole('combobox')
|
||||
fireEvent.click(selectButton)
|
||||
|
||||
// After opening, renderOption is called for each item, rendering the mocked StatusItem
|
||||
const statusItems = await screen.findAllByTestId('status-item')
|
||||
expect(statusItems.length).toBe(3)
|
||||
expect(statusItems[0]).toHaveTextContent('All')
|
||||
expect(statusItems[1]).toHaveTextContent('Enabled')
|
||||
expect(statusItems[2]).toHaveTextContent('Disabled')
|
||||
expect(await screen.findByRole('option', { name: 'All' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('option', { name: 'Enabled' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('option', { name: 'Disabled' })).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,12 +1,10 @@
|
||||
'use client'
|
||||
import { Checkbox } from '@langgenius/dify-ui/checkbox'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import Input from '@/app/components/base/input'
|
||||
import DisplayToggle from '../display-toggle'
|
||||
import StatusItem from '../status-item'
|
||||
import s from '../style.module.css'
|
||||
|
||||
type Item = {
|
||||
@ -67,15 +65,14 @@ function MenuBar({
|
||||
onChangeStatus(nextItem)
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className={cn(s.select, 'mr-2 h-fit')}>
|
||||
<SelectTrigger className="mr-2 w-[100px] shrink-0 shadow-none">
|
||||
{selectedStatus?.name ?? ''}
|
||||
</SelectTrigger>
|
||||
<SelectContent popupClassName="w-[160px]">
|
||||
{statusList.map(item => (
|
||||
<SelectItem key={item.value} value={String(item.value)} className="h-auto p-0">
|
||||
<SelectItemText className="sr-only m-0 p-0">{item.name}</SelectItemText>
|
||||
<StatusItem item={item} selected={item.value === selectDefaultValue} />
|
||||
{item.value === selectDefaultValue && <SelectItemIndicator className="hidden" />}
|
||||
<SelectItem key={item.value} value={String(item.value)}>
|
||||
<SelectItemText>{item.name}</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
|
||||
@ -1,27 +0,0 @@
|
||||
import type { FC } from 'react'
|
||||
import { RiCheckLine } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
|
||||
type StatusOption = {
|
||||
value: string | number
|
||||
name: string
|
||||
}
|
||||
|
||||
type IStatusItemProps = {
|
||||
item: StatusOption
|
||||
selected: boolean
|
||||
}
|
||||
|
||||
const StatusItem: FC<IStatusItemProps> = ({
|
||||
item,
|
||||
selected,
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex items-center justify-between px-2 py-1.5">
|
||||
<span className="system-md-regular">{item.name}</span>
|
||||
{selected && <RiCheckLine className="size-4 text-text-accent" />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(StatusItem)
|
||||
@ -33,9 +33,6 @@
|
||||
background: linear-gradient(to left, white, 90%, transparent);
|
||||
@apply text-primary-600 font-semibold text-xs absolute right-0 hidden h-12 pl-12 items-center;
|
||||
}
|
||||
.select {
|
||||
@apply !h-8 !w-[100px] !py-0 !pr-5 !shadow-none;
|
||||
}
|
||||
.segModalContent {
|
||||
@apply h-96 text-gray-800 text-base break-all overflow-y-scroll;
|
||||
white-space: pre-line;
|
||||
|
||||
@ -143,6 +143,18 @@ describe('SegmentAdd', () => {
|
||||
|
||||
expect(mockShowBatchModal).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should show plan upgrade modal instead of batch modal for sandbox users', async () => {
|
||||
mockPlan = { type: Plan.sandbox }
|
||||
const mockShowBatchModal = vi.fn()
|
||||
render(<SegmentAdd {...defaultProps} showBatchModal={mockShowBatchModal} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /list\.action\.batchAdd/i }))
|
||||
fireEvent.click(await screen.findByRole('menuitem', { name: /list\.action\.batchAdd/i }))
|
||||
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
||||
expect(mockShowBatchModal).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
// Disabled state (embedding)
|
||||
|
||||
@ -7,7 +7,7 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@langgenius/dify-ui/dropdown-menu'
|
||||
import { useRef, useState } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { PlanUpgradeModal } from '@/app/components/billing/plan-upgrade-modal'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
@ -30,9 +30,7 @@ export function SegmentAdd({
|
||||
embedding,
|
||||
}: SegmentAddProps) {
|
||||
const { t } = useTranslation()
|
||||
const [isBatchMenuOpen, setIsBatchMenuOpen] = useState(false)
|
||||
const [isPlanUpgradeModalOpen, setIsPlanUpgradeModalOpen] = useState(false)
|
||||
const batchMenuAnchorRef = useRef<HTMLDivElement>(null)
|
||||
const { plan, enableBilling } = useProviderContext()
|
||||
const canAddChunks = !enableBilling || plan.type !== Plan.sandbox
|
||||
|
||||
@ -40,24 +38,13 @@ export function SegmentAdd({
|
||||
? 'text-components-button-secondary-accent-text-disabled'
|
||||
: 'text-components-button-secondary-accent-text'
|
||||
|
||||
const handleAddClick = () => {
|
||||
const openSegmentDialog = (openDialog: () => void) => {
|
||||
if (!canAddChunks) {
|
||||
setIsPlanUpgradeModalOpen(true)
|
||||
return
|
||||
}
|
||||
|
||||
showNewSegmentModal()
|
||||
}
|
||||
|
||||
const handleBatchAddClick = () => {
|
||||
setIsBatchMenuOpen(false)
|
||||
|
||||
if (!canAddChunks) {
|
||||
setIsPlanUpgradeModalOpen(true)
|
||||
return
|
||||
}
|
||||
|
||||
showBatchModal()
|
||||
openDialog()
|
||||
}
|
||||
|
||||
if (importStatus) {
|
||||
@ -115,7 +102,6 @@ export function SegmentAdd({
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={batchMenuAnchorRef}
|
||||
className={cn(
|
||||
'relative z-20 flex items-center rounded-lg border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg shadow-xs shadow-shadow-shadow-3 backdrop-blur-[5px]',
|
||||
embedding && 'border-components-button-secondary-border-disabled bg-components-button-secondary-bg-disabled',
|
||||
@ -125,7 +111,7 @@ export function SegmentAdd({
|
||||
type="button"
|
||||
className={`inline-flex items-center rounded-l-lg border-0 border-r border-r-divider-subtle bg-transparent px-2.5 py-2 text-left
|
||||
hover:bg-state-base-hover disabled:cursor-not-allowed disabled:hover:bg-transparent`}
|
||||
onClick={handleAddClick}
|
||||
onClick={() => openSegmentDialog(showNewSegmentModal)}
|
||||
disabled={embedding}
|
||||
>
|
||||
<span aria-hidden className={cn('i-ri-add-line size-4', textColor)} />
|
||||
@ -133,29 +119,25 @@ export function SegmentAdd({
|
||||
{t('list.action.addButton', { ns: 'datasetDocuments' })}
|
||||
</span>
|
||||
</button>
|
||||
<DropdownMenu open={isBatchMenuOpen} onOpenChange={setIsBatchMenuOpen}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
aria-label={t('list.action.batchAdd', { ns: 'datasetDocuments' })}
|
||||
disabled={embedding}
|
||||
className={cn(
|
||||
`rounded-l-none rounded-r-lg border-0 bg-transparent p-2 backdrop-blur-[5px]
|
||||
hover:bg-state-base-hover disabled:cursor-not-allowed disabled:bg-transparent disabled:hover:bg-transparent`,
|
||||
isBatchMenuOpen && 'bg-state-base-hover',
|
||||
hover:bg-state-base-hover disabled:cursor-not-allowed disabled:bg-transparent disabled:hover:bg-transparent data-popup-open:bg-state-base-hover`,
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-center">
|
||||
<span aria-hidden className={cn('i-ri-arrow-down-s-line size-4', textColor)} />
|
||||
</div>
|
||||
<span aria-hidden className={cn('i-ri-arrow-down-s-line size-4', textColor)} />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
placement="bottom-start"
|
||||
placement="bottom-end"
|
||||
sideOffset={4}
|
||||
positionerProps={{ anchor: batchMenuAnchorRef }}
|
||||
popupClassName="w-[var(--anchor-width)]"
|
||||
popupClassName="min-w-[120px]"
|
||||
>
|
||||
<DropdownMenuItem
|
||||
className="system-md-regular"
|
||||
onClick={handleBatchAddClick}
|
||||
onClick={() => openSegmentDialog(showBatchModal)}
|
||||
>
|
||||
{t('list.action.batchAdd', { ns: 'datasetDocuments' })}
|
||||
</DropdownMenuItem>
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
@apply !p-2 !border-[0.5px] !border-components-button-secondary-border !bg-components-button-secondary-bg shadow-xs shadow-shadow-shadow-3 hover:!border-components-button-secondary-border-hover hover:!bg-components-button-secondary-bg-hover;
|
||||
}
|
||||
.actionItem {
|
||||
@apply h-9 py-2 px-3 mx-1 flex items-center gap-2 rounded-lg border-none bg-transparent text-left hover:bg-state-base-hover cursor-pointer;
|
||||
@apply h-9 w-[calc(100%-8px)] py-2 px-3 mx-1 flex items-center gap-2 rounded-lg border-none bg-transparent text-left hover:bg-state-base-hover cursor-pointer;
|
||||
}
|
||||
.deleteActionItem {
|
||||
@apply hover:!bg-state-destructive-hover;
|
||||
|
||||
@ -42,7 +42,7 @@ const OperationsDropdown = ({
|
||||
'border-components-actionbar-border bg-components-button-secondary-bg p-0 shadow-lg ring-2 shadow-shadow-shadow-5 ring-components-button-secondary-bg ring-inset',
|
||||
'transition-colors hover:border-components-actionbar-border hover:bg-state-base-hover',
|
||||
'focus-visible:bg-state-base-hover focus-visible:ring-1 focus-visible:ring-components-input-border-hover focus-visible:outline-hidden focus-visible:ring-inset',
|
||||
open && 'bg-state-base-hover',
|
||||
'data-popup-open:bg-state-base-hover',
|
||||
)}
|
||||
aria-label="Dataset operations"
|
||||
>
|
||||
|
||||
@ -101,7 +101,7 @@ const PermissionSelector = ({
|
||||
<div className="relative">
|
||||
<PopoverTrigger
|
||||
render={(
|
||||
<div className={cn('flex cursor-pointer items-center gap-x-0.5 rounded-lg bg-components-input-bg-normal px-2 py-1 hover:bg-state-base-hover-alt', open && 'bg-state-base-hover-alt', disabled && 'cursor-not-allowed! bg-components-input-bg-disabled! hover:bg-components-input-bg-disabled!')}>
|
||||
<div className={cn('group flex cursor-pointer items-center gap-x-0.5 rounded-lg bg-components-input-bg-normal px-2 py-1 hover:bg-state-base-hover-alt data-popup-open:bg-state-base-hover-alt', disabled && 'cursor-not-allowed! bg-components-input-bg-disabled! hover:bg-components-input-bg-disabled!')}>
|
||||
{
|
||||
isOnlyMe && (
|
||||
<>
|
||||
@ -169,8 +169,7 @@ const PermissionSelector = ({
|
||||
}
|
||||
<RiArrowDownSLine
|
||||
className={cn(
|
||||
'size-4 shrink-0 text-text-quaternary group-hover:text-text-secondary',
|
||||
open && 'text-text-secondary',
|
||||
'size-4 shrink-0 text-text-quaternary group-hover:text-text-secondary group-data-popup-open:text-text-secondary',
|
||||
disabled && 'text-components-input-text-placeholder!',
|
||||
)}
|
||||
/>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import ItemOperation from '../index'
|
||||
|
||||
@ -13,11 +13,21 @@ vi.mock('@langgenius/dify-ui/dropdown-menu', () => {
|
||||
}
|
||||
|
||||
return {
|
||||
DropdownMenu: ({ children, open, onOpenChange }: { children: React.ReactNode, open: boolean, onOpenChange?: (open: boolean) => void }) => (
|
||||
<DropdownMenuContext value={{ isOpen: open, setOpen: onOpenChange ?? vi.fn() }}>
|
||||
<div data-testid="dropdown-menu" data-open={open}>{children}</div>
|
||||
</DropdownMenuContext>
|
||||
),
|
||||
DropdownMenu: ({
|
||||
children,
|
||||
modal,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
modal?: boolean
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = React.useState(false)
|
||||
|
||||
return (
|
||||
<DropdownMenuContext value={{ isOpen, setOpen: setIsOpen }}>
|
||||
<div data-modal={modal} data-open={isOpen} data-testid="dropdown-menu">{children}</div>
|
||||
</DropdownMenuContext>
|
||||
)
|
||||
},
|
||||
DropdownMenuTrigger: ({
|
||||
children,
|
||||
onClick,
|
||||
@ -158,37 +168,41 @@ describe('ItemOperation', () => {
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should close the menu when mouse leaves the panel and item is not hovering', async () => {
|
||||
renderComponent()
|
||||
it('should keep the menu open when item hover leaves', async () => {
|
||||
const { props, rerender } = renderComponent({ isItemHovering: true })
|
||||
fireEvent.click(screen.getByTestId('item-operation-trigger'))
|
||||
await screen.findByText('explore.sidebar.action.pin')
|
||||
const menu = screen.getByTestId('dropdown-content')
|
||||
|
||||
fireEvent.mouseEnter(menu)
|
||||
fireEvent.mouseLeave(menu)
|
||||
rerender(<ItemOperation {...props} isItemHovering={false} />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('explore.sidebar.action.pin')).not.toBeInTheDocument()
|
||||
})
|
||||
expect(screen.getByText('explore.sidebar.action.pin')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should stop propagation when clicking inside the dropdown content', async () => {
|
||||
it('should render a non-modal menu', () => {
|
||||
renderComponent()
|
||||
|
||||
expect(screen.getByTestId('dropdown-menu')).toHaveAttribute('data-modal', 'false')
|
||||
})
|
||||
|
||||
it('should stop propagation when clicking menu actions', async () => {
|
||||
const onParentClick = vi.fn()
|
||||
const togglePin = vi.fn()
|
||||
|
||||
render(
|
||||
<div onClick={onParentClick}>
|
||||
<ItemOperation
|
||||
isPinned={false}
|
||||
isShowDelete
|
||||
togglePin={vi.fn()}
|
||||
togglePin={togglePin}
|
||||
onDelete={vi.fn()}
|
||||
/>
|
||||
</div>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('item-operation-trigger'))
|
||||
fireEvent.click(await screen.findByTestId('dropdown-content'))
|
||||
fireEvent.click(await screen.findByText('explore.sidebar.action.pin'))
|
||||
|
||||
expect(togglePin).toHaveBeenCalledTimes(1)
|
||||
expect(onParentClick).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@ -7,13 +7,7 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@langgenius/dify-ui/dropdown-menu'
|
||||
import {
|
||||
RiDeleteBinLine,
|
||||
RiEditLine,
|
||||
} from '@remixicon/react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Pin02 } from '../../base/icons/src/vender/line/general'
|
||||
import s from './style.module.css'
|
||||
@ -41,20 +35,17 @@ const ItemOperation: FC<IItemOperationProps> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation('explore')
|
||||
const { t: tCommon } = useTranslation('common')
|
||||
const [open, setOpen] = useState(false)
|
||||
const [isHovering, { setTrue: setIsHovering, setFalse: setNotHovering }] = useBoolean(false)
|
||||
useEffect(() => {
|
||||
if (!isItemHovering && !isHovering)
|
||||
setOpen(false)
|
||||
}, [isItemHovering, isHovering])
|
||||
|
||||
return (
|
||||
<DropdownMenu
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
>
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger
|
||||
data-testid="item-operation-trigger"
|
||||
className={cn(className, s.btn, 'size-6 rounded-md border-none py-1', (isItemHovering || open) && `${s.open} bg-components-actionbar-bg! shadow-none!`)}
|
||||
className={cn(
|
||||
s.btn,
|
||||
'size-6 rounded-md border-none py-1 data-popup-open:bg-components-actionbar-bg! data-popup-open:shadow-none!',
|
||||
isItemHovering && `${s.open} bg-components-actionbar-bg! shadow-none!`,
|
||||
className,
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
@ -65,11 +56,6 @@ const ItemOperation: FC<IItemOperationProps> = ({
|
||||
placement="bottom-end"
|
||||
sideOffset={4}
|
||||
popupClassName="min-w-[120px]"
|
||||
popupProps={{
|
||||
onMouseEnter: setIsHovering,
|
||||
onMouseLeave: setNotHovering,
|
||||
onClick: e => e.stopPropagation(),
|
||||
}}
|
||||
>
|
||||
<DropdownMenuItem
|
||||
className={cn(s.actionItem, 'gap-2 px-3')}
|
||||
@ -89,7 +75,7 @@ const ItemOperation: FC<IItemOperationProps> = ({
|
||||
onRenameConversation?.()
|
||||
}}
|
||||
>
|
||||
<RiEditLine className="size-4 shrink-0 text-text-secondary" />
|
||||
<span aria-hidden className="i-ri-edit-line size-4 shrink-0 text-text-secondary" />
|
||||
<span className={s.actionName}>{t('sidebar.action.rename')}</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
@ -101,7 +87,7 @@ const ItemOperation: FC<IItemOperationProps> = ({
|
||||
onDelete()
|
||||
}}
|
||||
>
|
||||
<RiDeleteBinLine className={cn(s.deleteActionItemChild, 'size-4 shrink-0 stroke-current stroke-2 text-inherit')} />
|
||||
<span aria-hidden className={cn(s.deleteActionItemChild, 'i-ri-delete-bin-line size-4 shrink-0 text-inherit')} />
|
||||
<span className={cn(s.actionName, s.deleteActionItemChild, 'text-inherit')}>{t('sidebar.action.delete')}</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
@ -20,6 +20,7 @@
|
||||
mask-image: url(~@/assets/action.svg);
|
||||
}
|
||||
|
||||
body .btn[data-popup-open],
|
||||
body .btn.open,
|
||||
body .btn:hover {
|
||||
background: url(~@/assets/action.svg) center center no-repeat transparent;
|
||||
|
||||
@ -42,8 +42,6 @@ vi.mock('@/config', async (importOriginal) => {
|
||||
})
|
||||
|
||||
describe('Support', () => {
|
||||
const mockCloseAccountDropdown = vi.fn()
|
||||
|
||||
const baseAppContextValue: AppContextValue = {
|
||||
userProfile: {
|
||||
id: '1',
|
||||
@ -105,7 +103,7 @@ describe('Support', () => {
|
||||
<DropdownMenu open={true} onOpenChange={() => { }}>
|
||||
<DropdownMenuTrigger>open</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<Support closeAccountDropdown={mockCloseAccountDropdown} />
|
||||
<Support />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>,
|
||||
)
|
||||
@ -189,7 +187,7 @@ describe('Support', () => {
|
||||
})
|
||||
|
||||
describe('Interactions and Links', () => {
|
||||
it('should call toggleZendeskWindow and closeAccountDropdown when "Contact Us" is clicked', () => {
|
||||
it('should call toggleZendeskWindow when "Contact Us" is clicked', () => {
|
||||
// Act
|
||||
renderSupport()
|
||||
fireEvent.click(screen.getByText('common.userProfile.support'))
|
||||
@ -197,7 +195,6 @@ describe('Support', () => {
|
||||
|
||||
// Assert
|
||||
expect(window.zE).toHaveBeenCalledWith('messenger', 'open')
|
||||
expect(mockCloseAccountDropdown).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should have correct forum and community links', () => {
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
|
||||
import type { MouseEventHandler, ReactNode } from 'react'
|
||||
import { Avatar } from '@langgenius/dify-ui/avatar'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLinkItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@langgenius/dify-ui/dropdown-menu'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useState } from 'react'
|
||||
@ -110,7 +109,6 @@ function AccountMenuSection({ children }: AccountMenuSectionProps) {
|
||||
export default function AppSelector() {
|
||||
const router = useRouter()
|
||||
const [aboutVisible, setAboutVisible] = useState(false)
|
||||
const [isAccountMenuOpen, setIsAccountMenuOpen] = useState(false)
|
||||
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||
|
||||
const { t } = useTranslation()
|
||||
@ -136,10 +134,10 @@ export default function AppSelector() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DropdownMenu open={isAccountMenuOpen} onOpenChange={setIsAccountMenuOpen}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
aria-label={t('account.account', { ns: 'common' })}
|
||||
className={cn('inline-flex items-center rounded-[20px] p-0.5 hover:bg-background-default-dodge', isAccountMenuOpen && 'bg-background-default-dodge')}
|
||||
className="inline-flex items-center rounded-[20px] p-0.5 hover:bg-background-default-dodge data-popup-open:bg-background-default-dodge"
|
||||
>
|
||||
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size="lg" />
|
||||
</DropdownMenuTrigger>
|
||||
@ -185,7 +183,7 @@ export default function AppSelector() {
|
||||
label={t('userProfile.helpCenter', { ns: 'common' })}
|
||||
trailing={<ExternalLinkIndicator />}
|
||||
/>
|
||||
<Support closeAccountDropdown={() => setIsAccountMenuOpen(false)} />
|
||||
<Support />
|
||||
{IS_CLOUD_EDITION && isCurrentWorkspaceOwner && <Compliance />}
|
||||
</AccountMenuSection>
|
||||
<DropdownMenuSeparator className="my-0! bg-divider-subtle" />
|
||||
@ -214,7 +212,6 @@ export default function AppSelector() {
|
||||
label={t('userProfile.about', { ns: 'common' })}
|
||||
onClick={() => {
|
||||
setAboutVisible(true)
|
||||
setIsAccountMenuOpen(false)
|
||||
}}
|
||||
trailing={(
|
||||
<div className="flex shrink-0 items-center">
|
||||
|
||||
@ -8,12 +8,8 @@ import { useProviderContext } from '@/context/provider-context'
|
||||
import { mailToSupport } from '../utils/util'
|
||||
import { ExternalLinkIndicator, MenuItemContent } from './menu-item-content'
|
||||
|
||||
type SupportProps = {
|
||||
closeAccountDropdown: () => void
|
||||
}
|
||||
|
||||
// Submenu-only: this component must be rendered within an existing DropdownMenu root.
|
||||
export default function Support({ closeAccountDropdown }: SupportProps) {
|
||||
export default function Support() {
|
||||
const { t } = useTranslation()
|
||||
const { plan } = useProviderContext()
|
||||
const { userProfile, langGeniusVersionInfo } = useAppContext()
|
||||
@ -37,7 +33,6 @@ export default function Support({ closeAccountDropdown }: SupportProps) {
|
||||
className="justify-between"
|
||||
onClick={() => {
|
||||
toggleZendeskWindow(true)
|
||||
closeAccountDropdown()
|
||||
}}
|
||||
>
|
||||
<MenuItemContent
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import type {
|
||||
DataSourceCredential,
|
||||
} from './types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@ -12,7 +11,6 @@ import {
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
@ -29,12 +27,10 @@ const Operator = ({
|
||||
onRename,
|
||||
}: OperatorProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
const {
|
||||
type,
|
||||
} = credentialItem
|
||||
const handleAction = useCallback((action: string) => {
|
||||
setOpen(false)
|
||||
queueMicrotask(() => {
|
||||
if (action === 'rename') {
|
||||
onRename?.()
|
||||
@ -45,12 +41,12 @@ const Operator = ({
|
||||
}, [credentialItem, onAction, onRename])
|
||||
|
||||
return (
|
||||
<DropdownMenu open={open} onOpenChange={setOpen}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={(
|
||||
<ActionButton
|
||||
size="l"
|
||||
className={cn(open && 'bg-state-base-hover', 'focus-visible:ring-2 focus-visible:ring-state-accent-solid')}
|
||||
className="focus-visible:ring-2 focus-visible:ring-state-accent-solid data-popup-open:bg-state-base-hover"
|
||||
aria-label={t('operation.more', { ns: 'common' })}
|
||||
>
|
||||
<span aria-hidden className="i-ri-more-fill size-4 text-text-tertiary" />
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
'use client'
|
||||
import type { Member } from '@/models/common'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@ -9,7 +8,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@langgenius/dify-ui/dropdown-menu'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { memo, useMemo, useState } from 'react'
|
||||
import { memo, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { deleteMemberOrCancelInvitation, updateMemberRole } from '@/service/common'
|
||||
@ -30,7 +29,6 @@ const nonOwnerRoles = ['admin', 'editor', 'normal'] as const
|
||||
const isNonOwnerRole = (role: Member['role']) => role !== 'owner'
|
||||
|
||||
const Operation = ({ member, operatorRole, onOperate }: IOperationProps) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const { t } = useTranslation()
|
||||
const { datasetOperatorEnabled } = useProviderContext()
|
||||
const RoleMap = {
|
||||
@ -59,7 +57,6 @@ const Operation = ({ member, operatorRole, onOperate }: IOperationProps) => {
|
||||
}, [operatorRole, datasetOperatorEnabled])
|
||||
const canRemoveMember = operatorRole === 'owner' || (operatorRole === 'admin' && isNonOwnerRole(member.role))
|
||||
const handleDeleteMemberOrCancelInvitation = async () => {
|
||||
setOpen(false)
|
||||
try {
|
||||
await deleteMemberOrCancelInvitation({ url: `/workspaces/current/members/${member.id}` })
|
||||
onOperate()
|
||||
@ -69,7 +66,6 @@ const Operation = ({ member, operatorRole, onOperate }: IOperationProps) => {
|
||||
}
|
||||
}
|
||||
const handleUpdateMemberRole = async (role: string) => {
|
||||
setOpen(false)
|
||||
try {
|
||||
await updateMemberRole({
|
||||
url: `/workspaces/current/members/${member.id}/update-role`,
|
||||
@ -82,12 +78,12 @@ const Operation = ({ member, operatorRole, onOperate }: IOperationProps) => {
|
||||
}
|
||||
}
|
||||
return (
|
||||
<DropdownMenu open={open} onOpenChange={setOpen}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={<button type="button" className={cn('group flex size-full cursor-pointer items-center justify-between border-none bg-transparent px-3 text-left system-sm-regular text-text-secondary hover:bg-state-base-hover', open && 'bg-state-base-hover')} />}
|
||||
className="group flex size-full cursor-pointer items-center justify-between border-none bg-transparent px-3 text-left system-sm-regular text-text-secondary hover:bg-state-base-hover data-popup-open:bg-state-base-hover"
|
||||
>
|
||||
{RoleMap[member.role] || RoleMap.normal}
|
||||
<span aria-hidden className={cn('i-ri-arrow-down-s-line size-4 shrink-0 group-hover:block', open ? 'block' : 'hidden')} />
|
||||
<span aria-hidden className="i-ri-arrow-down-s-line hidden size-4 shrink-0 group-hover:block group-data-popup-open:block" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
placement="bottom-end"
|
||||
|
||||
@ -117,7 +117,8 @@ describe('MemberSelector', () => {
|
||||
const trigger = screen.getByTestId('member-selector-trigger')
|
||||
await user.click(trigger)
|
||||
|
||||
expect(trigger).toHaveClass('bg-state-base-hover-alt')
|
||||
expect(trigger).toHaveAttribute('data-popup-open')
|
||||
expect(trigger).toHaveClass('data-popup-open:bg-state-base-hover-alt')
|
||||
})
|
||||
|
||||
it('should not match account when neither name nor email contains search value', async () => {
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { Avatar } from '@langgenius/dify-ui/avatar'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
@ -56,7 +55,7 @@ const MemberSelector: FC<Props> = ({
|
||||
render={(
|
||||
<div
|
||||
data-testid="member-selector-trigger"
|
||||
className={cn('group flex cursor-pointer items-center gap-1.5 rounded-lg bg-components-input-bg-normal px-2 py-1 hover:bg-state-base-hover-alt', open && 'bg-state-base-hover-alt')}
|
||||
className="group flex cursor-pointer items-center gap-1.5 rounded-lg bg-components-input-bg-normal px-2 py-1 hover:bg-state-base-hover-alt data-popup-open:bg-state-base-hover-alt"
|
||||
>
|
||||
{!currentValue && (
|
||||
<div className="grow p-1 system-sm-regular text-components-input-text-placeholder">{t('members.transferModal.transferPlaceholder', { ns: 'common' })}</div>
|
||||
@ -68,7 +67,7 @@ const MemberSelector: FC<Props> = ({
|
||||
<div className="system-xs-regular text-text-quaternary">{currentValue.email}</div>
|
||||
</>
|
||||
)}
|
||||
<div className={cn('i-ri-arrow-down-s-line size-4 text-text-quaternary group-hover:text-text-secondary', open && 'text-text-secondary')} />
|
||||
<div className="i-ri-arrow-down-s-line size-4 text-text-quaternary group-hover:text-text-secondary group-data-popup-open:text-text-secondary" />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
@ -39,16 +39,15 @@ const OperationDropdown: FC<Props> = ({
|
||||
popupClassName,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const { data: enable_marketplace } = useSuspenseQuery({
|
||||
...systemFeaturesQueryOptions(),
|
||||
select: s => s.enable_marketplace,
|
||||
})
|
||||
|
||||
return (
|
||||
<DropdownMenu open={open} onOpenChange={setOpen}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
className={cn('action-btn action-btn-m', open && 'bg-state-base-hover')}
|
||||
className="action-btn action-btn-m data-popup-open:bg-state-base-hover"
|
||||
>
|
||||
<span className="i-ri-more-fill size-4" />
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
@ -4,7 +4,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
|
||||
import InstallPluginDropdown from '../install-plugin-dropdown'
|
||||
|
||||
let portalOpen = false
|
||||
const {
|
||||
mockSystemFeatures,
|
||||
} = vi.hoisted(() => ({
|
||||
@ -63,15 +62,22 @@ vi.mock('@langgenius/dify-ui/dropdown-menu', async () => {
|
||||
modal,
|
||||
children,
|
||||
}: {
|
||||
open: boolean
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
modal?: boolean
|
||||
children: React.ReactNode
|
||||
}) => {
|
||||
portalOpen = open
|
||||
const [internalOpen, setInternalOpen] = React.useState(open ?? false)
|
||||
const isOpen = open ?? internalOpen
|
||||
const setOpen = (nextOpen: boolean) => {
|
||||
if (open === undefined)
|
||||
setInternalOpen(nextOpen)
|
||||
onOpenChange?.(nextOpen)
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenuContext value={{ isOpen: open, setOpen: onOpenChange ?? vi.fn() }}>
|
||||
<div data-testid="dropdown-menu" data-open={open} data-modal={modal}>{children}</div>
|
||||
<DropdownMenuContext value={{ isOpen, setOpen }}>
|
||||
<div data-testid="dropdown-menu" data-open={isOpen} data-modal={modal}>{children}</div>
|
||||
</DropdownMenuContext>
|
||||
)
|
||||
},
|
||||
@ -99,7 +105,10 @@ vi.mock('@langgenius/dify-ui/dropdown-menu', async () => {
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) => portalOpen ? <div data-testid="dropdown-content">{children}</div> : null,
|
||||
}) => {
|
||||
const { isOpen } = useDropdownMenuContext()
|
||||
return isOpen ? <div data-testid="dropdown-content">{children}</div> : null
|
||||
},
|
||||
DropdownMenuItem: ({
|
||||
children,
|
||||
onClick,
|
||||
@ -150,7 +159,6 @@ vi.mock('@/app/components/plugins/install-plugin/install-from-local-package', ()
|
||||
describe('InstallPluginDropdown', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
portalOpen = false
|
||||
mockSystemFeatures.enable_marketplace = true
|
||||
mockSystemFeatures.plugin_installation_permission.restrict_to_marketplace_only = false
|
||||
})
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@ -11,7 +10,7 @@ import {
|
||||
import { RiAddLine, RiArrowDownSLine } from '@remixicon/react'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { FileZip } from '@/app/components/base/icons/src/vender/solid/files'
|
||||
import { Github } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
@ -36,7 +35,6 @@ const InstallPluginDropdown = ({
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false)
|
||||
const [selectedAction, setSelectedAction] = useState<string | null>(null)
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
||||
const { data: enable_marketplace } = useSuspenseQuery({
|
||||
@ -54,7 +52,6 @@ const InstallPluginDropdown = ({
|
||||
if (file) {
|
||||
setSelectedFile(file)
|
||||
setSelectedAction('local')
|
||||
setIsMenuOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
@ -78,20 +75,17 @@ const InstallPluginDropdown = ({
|
||||
// console.log(res)
|
||||
// }
|
||||
|
||||
const [installMethods, setInstallMethods] = useState<InstallMethod[]>([])
|
||||
useEffect(() => {
|
||||
const methods = []
|
||||
const installMethods = useMemo<InstallMethod[]>(() => {
|
||||
const methods: InstallMethod[] = []
|
||||
if (enable_marketplace)
|
||||
methods.push({ icon: MagicBox, text: t('source.marketplace', { ns: 'plugin' }), action: 'marketplace' })
|
||||
|
||||
if (plugin_installation_permission.restrict_to_marketplace_only) {
|
||||
setInstallMethods(methods)
|
||||
}
|
||||
else {
|
||||
methods.push({ icon: Github, text: t('source.github', { ns: 'plugin' }), action: 'github' })
|
||||
methods.push({ icon: FileZip, text: t('source.local', { ns: 'plugin' }), action: 'local' })
|
||||
setInstallMethods(methods)
|
||||
}
|
||||
if (plugin_installation_permission.restrict_to_marketplace_only)
|
||||
return methods
|
||||
|
||||
methods.push({ icon: Github, text: t('source.github', { ns: 'plugin' }), action: 'github' })
|
||||
methods.push({ icon: FileZip, text: t('source.local', { ns: 'plugin' }), action: 'local' })
|
||||
return methods
|
||||
}, [plugin_installation_permission, enable_marketplace, t])
|
||||
|
||||
const handleInstallMethodSelect = (action: string) => {
|
||||
@ -111,7 +105,7 @@ const InstallPluginDropdown = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu open={isMenuOpen} onOpenChange={setIsMenuOpen} modal={false}>
|
||||
<DropdownMenu modal={false}>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="file"
|
||||
@ -123,7 +117,7 @@ const InstallPluginDropdown = ({
|
||||
<DropdownMenuTrigger
|
||||
render={(
|
||||
<Button
|
||||
className={cn('size-full p-2 text-components-button-secondary-text', isMenuOpen && 'bg-state-base-hover')}
|
||||
className="size-full p-2 text-components-button-secondary-text data-popup-open:bg-state-base-hover"
|
||||
/>
|
||||
)}
|
||||
>
|
||||
|
||||
@ -233,25 +233,6 @@ describe('MenuDropdown', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('forceClose prop', () => {
|
||||
it('should close dropdown when forceClose changes to true', async () => {
|
||||
const { rerender } = render(<MenuDropdown data={baseSiteInfo} forceClose={false} />)
|
||||
|
||||
const triggerButton = screen.getByRole('button')
|
||||
fireEvent.click(triggerButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('common.theme.theme')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
rerender(<MenuDropdown data={baseSiteInfo} forceClose={true} />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('common.theme.theme')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('placement prop', () => {
|
||||
it('should accept custom placement', () => {
|
||||
render(<MenuDropdown data={baseSiteInfo} placement="top-start" />)
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
import type { Placement } from '@langgenius/dify-ui/dropdown-menu'
|
||||
import type { FC } from 'react'
|
||||
import type { SiteInfo } from '@/models/share'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@ -12,7 +11,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@langgenius/dify-ui/dropdown-menu'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import ThemeSwitcher from '@/app/components/base/theme-switcher'
|
||||
@ -26,50 +25,37 @@ type Props = {
|
||||
data?: SiteInfo
|
||||
placement?: Placement
|
||||
hideLogout?: boolean
|
||||
forceClose?: boolean
|
||||
}
|
||||
|
||||
const MenuDropdown: FC<Props> = ({
|
||||
data,
|
||||
placement,
|
||||
hideLogout,
|
||||
forceClose,
|
||||
}) => {
|
||||
const webAppAccessMode = useWebAppStore(s => s.webAppAccessMode)
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const shareCode = useWebAppStore(s => s.shareCode)
|
||||
const handleLogout = useCallback(async () => {
|
||||
setOpen(false)
|
||||
const handleLogout = async () => {
|
||||
await webAppLogout(shareCode!)
|
||||
router.replace(`/webapp-signin?redirect_url=${pathname}`)
|
||||
}, [pathname, router, setOpen, shareCode])
|
||||
}
|
||||
|
||||
const [show, setShow] = useState(false)
|
||||
const handleOpenInfoModal = useCallback(() => {
|
||||
setOpen(false)
|
||||
const handleOpenInfoModal = () => {
|
||||
queueMicrotask(() => {
|
||||
setShow(true)
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (forceClose)
|
||||
setOpen(false)
|
||||
}, [forceClose, setOpen])
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={(
|
||||
<ActionButton size="l" className={cn(open && 'bg-state-base-hover')}>
|
||||
<ActionButton size="l" className="data-popup-open:bg-state-base-hover">
|
||||
<span aria-hidden className="i-ri-equalizer-2-line h-[18px] w-[18px]" />
|
||||
</ActionButton>
|
||||
)}
|
||||
|
||||
@ -14,11 +14,21 @@ vi.mock('@langgenius/dify-ui/dropdown-menu', async () => {
|
||||
}
|
||||
|
||||
return {
|
||||
DropdownMenu: ({ children, open, onOpenChange }: { children: React.ReactNode, open: boolean, onOpenChange?: (open: boolean) => void }) => (
|
||||
<DropdownMenuContext value={{ isOpen: open, setOpen: onOpenChange ?? vi.fn() }}>
|
||||
<div data-testid="dropdown-menu" data-open={open}>{children}</div>
|
||||
</DropdownMenuContext>
|
||||
),
|
||||
DropdownMenu: ({ children, open, onOpenChange }: { children: React.ReactNode, open?: boolean, onOpenChange?: (open: boolean) => void }) => {
|
||||
const [internalOpen, setInternalOpen] = React.useState(open ?? false)
|
||||
const isOpen = open ?? internalOpen
|
||||
const setOpen = (nextOpen: boolean) => {
|
||||
if (open === undefined)
|
||||
setInternalOpen(nextOpen)
|
||||
onOpenChange?.(nextOpen)
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenuContext value={{ isOpen, setOpen }}>
|
||||
<div data-testid="dropdown-menu" data-open={isOpen}>{children}</div>
|
||||
</DropdownMenuContext>
|
||||
)
|
||||
},
|
||||
DropdownMenuTrigger: ({
|
||||
children,
|
||||
render,
|
||||
|
||||
@ -13,7 +13,6 @@ import {
|
||||
RiMoreFill,
|
||||
} from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
|
||||
@ -31,16 +30,11 @@ const OperationDropdown: FC<Props> = ({
|
||||
onRemove,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
const handleOpenChange = useCallback((nextOpen: boolean) => {
|
||||
setOpen(nextOpen)
|
||||
onOpenChange?.(nextOpen)
|
||||
}, [onOpenChange])
|
||||
|
||||
return (
|
||||
<DropdownMenu open={open} onOpenChange={handleOpenChange}>
|
||||
<DropdownMenu onOpenChange={onOpenChange}>
|
||||
<DropdownMenuTrigger
|
||||
render={<ActionButton size={inCard ? 'l' : 'm'} className={cn(open && 'bg-state-base-hover')} />}
|
||||
render={<ActionButton size={inCard ? 'l' : 'm'} className="data-popup-open:bg-state-base-hover" />}
|
||||
>
|
||||
<RiMoreFill className={cn('size-4', inCard && 'size-5')} />
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
@ -3,11 +3,10 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useDownloadPlugin } from '@/service/use-plugins'
|
||||
import OperationDropdown from '../action'
|
||||
|
||||
const mockDownloadBlob = vi.fn()
|
||||
const mockRemoveQueries = vi.fn()
|
||||
const mockDownloadPlugin = vi.fn()
|
||||
|
||||
vi.mock('next-themes', () => ({
|
||||
useTheme: () => ({
|
||||
@ -15,8 +14,15 @@ vi.mock('next-themes', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-plugins', () => ({
|
||||
useDownloadPlugin: vi.fn(),
|
||||
vi.mock('@/service/client', () => ({
|
||||
marketplaceQuery: {
|
||||
downloadPlugin: {
|
||||
mutationOptions: (options = {}) => ({
|
||||
mutationFn: (input: unknown) => mockDownloadPlugin(input),
|
||||
...options,
|
||||
}),
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/download', () => ({
|
||||
@ -37,9 +43,6 @@ const createQueryClient = () => new QueryClient({
|
||||
|
||||
const renderComponent = (props?: Partial<ComponentProps<typeof OperationDropdown>>) => {
|
||||
const queryClient = createQueryClient()
|
||||
vi.spyOn(queryClient, 'removeQueries').mockImplementation(((...args) => {
|
||||
return mockRemoveQueries(...args)
|
||||
}) as typeof queryClient.removeQueries)
|
||||
|
||||
return render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
@ -58,10 +61,7 @@ const renderComponent = (props?: Partial<ComponentProps<typeof OperationDropdown
|
||||
describe('OperationDropdown', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(useDownloadPlugin).mockImplementation((_, enabled) => ({
|
||||
data: enabled ? null : null,
|
||||
isLoading: false,
|
||||
}) as unknown as ReturnType<typeof useDownloadPlugin>)
|
||||
mockDownloadPlugin.mockResolvedValue(new Blob(['plugin zip'], { type: 'application/zip' }))
|
||||
})
|
||||
|
||||
it('should render download and view details actions when opened', async () => {
|
||||
@ -78,28 +78,35 @@ describe('OperationDropdown', () => {
|
||||
await userEvent.setup().click(screen.getByText('common.operation.download'))
|
||||
|
||||
expect(onOpenChange).toHaveBeenCalledWith(false)
|
||||
expect(mockRemoveQueries).toHaveBeenCalled()
|
||||
await waitFor(() => {
|
||||
expect(mockDownloadPlugin).toHaveBeenCalledWith({
|
||||
params: {
|
||||
organization: 'langgenius',
|
||||
pluginName: 'test-plugin',
|
||||
version: '1.0.0',
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should skip download when already loading', async () => {
|
||||
vi.mocked(useDownloadPlugin).mockReturnValue({
|
||||
data: null,
|
||||
isLoading: true,
|
||||
} as unknown as ReturnType<typeof useDownloadPlugin>)
|
||||
it('should skip duplicate downloads while pending', async () => {
|
||||
mockDownloadPlugin.mockReturnValue(new Promise(() => {}))
|
||||
|
||||
renderComponent({ open: true })
|
||||
|
||||
await userEvent.setup().click(screen.getByText('common.operation.download'))
|
||||
const user = userEvent.setup()
|
||||
await user.click(screen.getByText('common.operation.download'))
|
||||
|
||||
expect(mockRemoveQueries).not.toHaveBeenCalled()
|
||||
await waitFor(() => {
|
||||
expect(mockDownloadPlugin).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
await user.click(screen.getByText('common.operation.download'))
|
||||
|
||||
expect(mockDownloadPlugin).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should download the blob when the hook returns data', async () => {
|
||||
vi.mocked(useDownloadPlugin).mockImplementation((_, enabled) => ({
|
||||
data: enabled ? new Blob(['plugin zip'], { type: 'application/zip' }) : null,
|
||||
isLoading: false,
|
||||
}) as unknown as ReturnType<typeof useDownloadPlugin>)
|
||||
|
||||
it('should download the blob when the request returns data', async () => {
|
||||
renderComponent({ open: true })
|
||||
|
||||
await userEvent.setup().click(screen.getByText('common.operation.download'))
|
||||
@ -110,7 +117,6 @@ describe('OperationDropdown', () => {
|
||||
fileName: 'langgenius-test-plugin_1.0.0.zip',
|
||||
})
|
||||
})
|
||||
expect(mockRemoveQueries).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should link to the marketplace detail page', () => {
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@ -8,13 +7,12 @@ import {
|
||||
DropdownMenuLinkItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@langgenius/dify-ui/dropdown-menu'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { useTheme } from 'next-themes'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import { useDownloadPlugin } from '@/service/use-plugins'
|
||||
import { marketplaceQuery } from '@/service/client'
|
||||
import { downloadBlob } from '@/utils/download'
|
||||
import { getMarketplaceUrl } from '@/utils/var'
|
||||
|
||||
@ -35,49 +33,36 @@ const OperationDropdown: FC<Props> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
const queryClient = useQueryClient()
|
||||
const setOpen = useCallback((value: boolean) => {
|
||||
onOpenChange(value)
|
||||
}, [onOpenChange])
|
||||
|
||||
const [needDownload, setNeedDownload] = useState(false)
|
||||
const downloadInfo = useMemo(() => ({
|
||||
organization: author,
|
||||
pluginName: name,
|
||||
version,
|
||||
}), [author, name, version])
|
||||
const { data: blob, isLoading } = useDownloadPlugin(downloadInfo, needDownload)
|
||||
const handleDownload = useCallback(() => {
|
||||
if (isLoading)
|
||||
return
|
||||
setOpen(false)
|
||||
queryClient.removeQueries({
|
||||
queryKey: ['plugins', 'downloadPlugin', downloadInfo],
|
||||
exact: true,
|
||||
})
|
||||
setNeedDownload(true)
|
||||
}, [downloadInfo, isLoading, queryClient, setOpen])
|
||||
const downloadMutation = useMutation(marketplaceQuery.downloadPlugin.mutationOptions({
|
||||
onSuccess: (blob) => {
|
||||
downloadBlob({ data: blob, fileName: `${author}-${name}_${version}.zip` })
|
||||
},
|
||||
}))
|
||||
|
||||
useEffect(() => {
|
||||
if (!needDownload || !blob)
|
||||
const handleDownload = () => {
|
||||
if (downloadMutation.isPending)
|
||||
return
|
||||
const fileName = `${author}-${name}_${version}.zip`
|
||||
downloadBlob({ data: blob, fileName })
|
||||
setNeedDownload(false)
|
||||
queryClient.removeQueries({
|
||||
queryKey: ['plugins', 'downloadPlugin', downloadInfo],
|
||||
exact: true,
|
||||
|
||||
onOpenChange(false)
|
||||
downloadMutation.mutate({
|
||||
params: {
|
||||
organization: author,
|
||||
pluginName: name,
|
||||
version,
|
||||
},
|
||||
})
|
||||
}, [author, blob, downloadInfo, name, needDownload, queryClient, version])
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
onOpenChange={onOpenChange}
|
||||
>
|
||||
<DropdownMenuTrigger
|
||||
render={(
|
||||
<ActionButton
|
||||
className={cn(open && 'bg-state-base-hover', 'focus-visible:ring-2 focus-visible:ring-state-accent-solid')}
|
||||
className="focus-visible:ring-2 focus-visible:ring-state-accent-solid data-popup-open:bg-state-base-hover"
|
||||
aria-label={t('operation.more', { ns: 'common' })}
|
||||
>
|
||||
<span aria-hidden className="i-ri-more-fill size-4 text-components-button-secondary-accent-text" />
|
||||
|
||||
@ -54,7 +54,7 @@ const OperationSelector: FC<OperationSelectorProps> = ({
|
||||
>
|
||||
<DropdownMenuTrigger
|
||||
disabled={disabled}
|
||||
className={cn('flex items-center gap-0.5 rounded-lg bg-components-input-bg-normal px-2 py-1', disabled ? 'cursor-not-allowed bg-components-input-bg-disabled!' : 'cursor-pointer hover:bg-state-base-hover-alt', open && 'bg-state-base-hover-alt', className)}
|
||||
className={cn('group flex items-center gap-0.5 rounded-lg bg-components-input-bg-normal px-2 py-1 data-popup-open:bg-state-base-hover-alt', disabled ? 'cursor-not-allowed bg-components-input-bg-disabled!' : 'cursor-pointer hover:bg-state-base-hover-alt', className)}
|
||||
>
|
||||
<div className="flex items-center p-1">
|
||||
<span
|
||||
@ -64,7 +64,7 @@ const OperationSelector: FC<OperationSelectorProps> = ({
|
||||
{selectedItem && isOperationItem(selectedItem) ? t(`nodes.assigner.operations.${selectedItem.name}`, { ns: 'workflow' }) : t('nodes.assigner.operations.title', { ns: 'workflow' })}
|
||||
</span>
|
||||
</div>
|
||||
<span aria-hidden className={cn('i-ri-arrow-down-s-line size-4 text-text-quaternary', disabled && 'text-components-input-text-placeholder', open && 'text-text-secondary')} />
|
||||
<span aria-hidden className={cn('i-ri-arrow-down-s-line size-4 text-text-quaternary group-data-popup-open:text-text-secondary', disabled && 'text-components-input-text-placeholder')} />
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent
|
||||
|
||||
@ -69,7 +69,7 @@ const MethodSelector: FC<MethodSelectorProps> = ({
|
||||
render={(
|
||||
<ActionButton
|
||||
aria-label={t(`${i18nPrefix}.deliveryMethod.title`, { ns: 'workflow' })}
|
||||
className={cn(open && 'bg-state-base-hover')}
|
||||
className="data-popup-open:bg-state-base-hover"
|
||||
>
|
||||
<RiAddLine className="size-4" />
|
||||
</ActionButton>
|
||||
|
||||
@ -1,60 +1,8 @@
|
||||
import type { Member } from '@/models/common'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import MemberSelector from '../member-selector'
|
||||
|
||||
vi.mock('@langgenius/dify-ui/popover', async () => {
|
||||
const React = await import('react')
|
||||
const PopoverContext = React.createContext({
|
||||
open: false,
|
||||
setOpen: (_open: boolean) => {},
|
||||
})
|
||||
|
||||
const Popover = ({
|
||||
children,
|
||||
open: controlledOpen,
|
||||
onOpenChange,
|
||||
}: {
|
||||
children: import('react').ReactNode
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}) => {
|
||||
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false)
|
||||
const isControlled = controlledOpen !== undefined
|
||||
const open = isControlled ? !!controlledOpen : uncontrolledOpen
|
||||
const setOpen = (nextOpen: boolean) => {
|
||||
if (!isControlled)
|
||||
setUncontrolledOpen(nextOpen)
|
||||
onOpenChange?.(nextOpen)
|
||||
}
|
||||
|
||||
return (
|
||||
<PopoverContext.Provider value={{ open, setOpen }}>
|
||||
{children}
|
||||
</PopoverContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const PopoverTrigger = ({ render }: { render: import('react').ReactNode }) => {
|
||||
const { open, setOpen } = React.useContext(PopoverContext)
|
||||
return (
|
||||
<div onClick={() => setOpen(!open)}>
|
||||
{render}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const PopoverContent = ({ children }: { children: import('react').ReactNode }) => {
|
||||
const { open } = React.useContext(PopoverContext)
|
||||
return open ? <div data-testid="popover-content">{children}</div> : null
|
||||
}
|
||||
|
||||
return {
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
PopoverContent,
|
||||
}
|
||||
})
|
||||
|
||||
const mockMemberList = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('../member-list', () => ({
|
||||
@ -87,7 +35,9 @@ describe('human-input/delivery-method/recipient/member-selector', () => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should toggle the member list and forward selection props', () => {
|
||||
it('should toggle the member list and forward selection props', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<MemberSelector
|
||||
value={[{ type: 'member', user_id: 'member-1' }]}
|
||||
@ -103,16 +53,17 @@ describe('human-input/delivery-method/recipient/member-selector', () => {
|
||||
|
||||
expect(screen.queryByTestId('member-list')).not.toBeInTheDocument()
|
||||
|
||||
fireEvent.click(trigger)
|
||||
await user.click(trigger)
|
||||
expect(screen.getByTestId('member-list')).toBeInTheDocument()
|
||||
expect(trigger).toHaveClass('bg-state-accent-hover')
|
||||
expect(trigger).toHaveAttribute('data-popup-open')
|
||||
expect(trigger).toHaveClass('data-popup-open:bg-state-accent-hover')
|
||||
expect(mockMemberList).toHaveBeenCalledWith(expect.objectContaining({
|
||||
searchValue: '',
|
||||
list: members,
|
||||
email: 'owner@example.com',
|
||||
}))
|
||||
|
||||
fireEvent.click(trigger)
|
||||
await user.click(trigger)
|
||||
expect(screen.queryByTestId('member-list')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -3,7 +3,6 @@ import type { FC } from 'react'
|
||||
import type { Recipient } from '@/app/components/workflow/nodes/human-input/types'
|
||||
import type { Member } from '@/models/common'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
@ -45,7 +44,7 @@ const MemberSelector: FC<Props> = ({
|
||||
<PopoverTrigger
|
||||
render={(
|
||||
<Button
|
||||
className={cn('w-full justify-between', open && 'bg-state-accent-hover')}
|
||||
className="w-full justify-between data-popup-open:bg-state-accent-hover"
|
||||
variant="ghost-accent"
|
||||
>
|
||||
<RiContactsBookLine className="mr-1 size-4" />
|
||||
|
||||
@ -82,8 +82,18 @@ vi.mock('@langgenius/dify-ui/dropdown-menu', async () => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={ariaLabel}
|
||||
className={className}
|
||||
onClick={() => setOpen(!open)}
|
||||
onMouseDown={(event) => {
|
||||
const baseUiEvent = event as MouseEvent<HTMLButtonElement> & { preventBaseUIHandler?: () => void }
|
||||
baseUiEvent.preventBaseUIHandler = vi.fn()
|
||||
onMouseDown?.(baseUiEvent as unknown as MouseEvent<HTMLDivElement>)
|
||||
}}
|
||||
onClick={(event) => {
|
||||
onClick?.(event as unknown as MouseEvent<HTMLDivElement>)
|
||||
if (!onMouseDown)
|
||||
setOpen(!open)
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
|
||||
@ -37,12 +37,10 @@ const Operator = ({
|
||||
onOpenChange={setOpen}
|
||||
>
|
||||
<DropdownMenuTrigger
|
||||
nativeButton={false}
|
||||
render={<div />}
|
||||
aria-label={t('operation.more', { ns: 'common' })}
|
||||
className={cn(
|
||||
'flex size-8 cursor-pointer items-center justify-center rounded-lg text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary',
|
||||
open && 'bg-state-base-hover text-text-secondary',
|
||||
'data-popup-open:bg-state-base-hover data-popup-open:text-text-secondary',
|
||||
)}
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault()
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import type { FC } from 'react'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@ -11,7 +10,6 @@ import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import {
|
||||
Fragment,
|
||||
memo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
@ -26,17 +24,19 @@ import {
|
||||
import { ShortcutKbd } from '../shortcuts/shortcut-kbd'
|
||||
import TipPopup from './tip-popup'
|
||||
|
||||
enum ZoomType {
|
||||
zoomToFit = 'zoomToFit',
|
||||
zoomTo25 = 'zoomTo25',
|
||||
zoomTo50 = 'zoomTo50',
|
||||
zoomTo75 = 'zoomTo75',
|
||||
zoomTo100 = 'zoomTo100',
|
||||
zoomTo200 = 'zoomTo200',
|
||||
toggleUserComments = 'toggleUserComments',
|
||||
toggleUserCursors = 'toggleUserCursors',
|
||||
toggleMiniMap = 'toggleMiniMap',
|
||||
}
|
||||
const ZoomType = {
|
||||
zoomToFit: 'zoomToFit',
|
||||
zoomTo25: 'zoomTo25',
|
||||
zoomTo50: 'zoomTo50',
|
||||
zoomTo75: 'zoomTo75',
|
||||
zoomTo100: 'zoomTo100',
|
||||
zoomTo200: 'zoomTo200',
|
||||
toggleUserComments: 'toggleUserComments',
|
||||
toggleUserCursors: 'toggleUserCursors',
|
||||
toggleMiniMap: 'toggleMiniMap',
|
||||
} as const
|
||||
|
||||
type ZoomType = typeof ZoomType[keyof typeof ZoomType]
|
||||
|
||||
type ZoomInOutProps = {
|
||||
showMiniMap?: boolean
|
||||
@ -66,7 +66,6 @@ const ZoomInOut: FC<ZoomInOutProps> = ({
|
||||
} = useReactFlow()
|
||||
const { zoom } = useViewport()
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const [open, setOpen] = useState(false)
|
||||
const {
|
||||
workflowReadOnly,
|
||||
getWorkflowReadOnly,
|
||||
@ -126,12 +125,10 @@ const ZoomInOut: FC<ZoomInOutProps> = ({
|
||||
],
|
||||
]
|
||||
|
||||
const handleZoom = (type: string) => {
|
||||
const handleZoom = (type: ZoomType) => {
|
||||
if (workflowReadOnly)
|
||||
return
|
||||
|
||||
setOpen(false)
|
||||
|
||||
if (type === ZoomType.zoomToFit)
|
||||
fitView()
|
||||
|
||||
@ -199,16 +196,10 @@ const ZoomInOut: FC<ZoomInOutProps> = ({
|
||||
<span aria-hidden className="i-ri-zoom-out-line size-4 text-text-tertiary hover:text-text-secondary" />
|
||||
</button>
|
||||
</TipPopup>
|
||||
<DropdownMenu
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
disabled={getWorkflowReadOnly()}
|
||||
className={cn(
|
||||
'flex h-8 w-[34px] items-center justify-center rounded-lg system-sm-medium text-text-tertiary hover:bg-black/5 hover:text-text-secondary',
|
||||
open && 'bg-black/5 text-text-secondary',
|
||||
)}
|
||||
className="flex h-8 w-[34px] items-center justify-center rounded-lg system-sm-medium text-text-tertiary hover:bg-black/5 hover:text-text-secondary data-popup-open:bg-black/5 data-popup-open:text-text-secondary"
|
||||
>
|
||||
{Number.parseFloat(`${zoom * 100}`).toFixed(0)}
|
||||
%
|
||||
|
||||
@ -10,7 +10,6 @@ import {
|
||||
import {
|
||||
Fragment,
|
||||
memo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
@ -20,14 +19,16 @@ import {
|
||||
import TipPopup from '@/app/components/workflow/operator/tip-popup'
|
||||
import { ShortcutKbd } from '@/app/components/workflow/shortcuts/shortcut-kbd'
|
||||
|
||||
enum ZoomType {
|
||||
zoomToFit = 'zoomToFit',
|
||||
zoomTo25 = 'zoomTo25',
|
||||
zoomTo50 = 'zoomTo50',
|
||||
zoomTo75 = 'zoomTo75',
|
||||
zoomTo100 = 'zoomTo100',
|
||||
zoomTo200 = 'zoomTo200',
|
||||
}
|
||||
const ZoomType = {
|
||||
zoomToFit: 'zoomToFit',
|
||||
zoomTo25: 'zoomTo25',
|
||||
zoomTo50: 'zoomTo50',
|
||||
zoomTo75: 'zoomTo75',
|
||||
zoomTo100: 'zoomTo100',
|
||||
zoomTo200: 'zoomTo200',
|
||||
} as const
|
||||
|
||||
type ZoomType = typeof ZoomType[keyof typeof ZoomType]
|
||||
|
||||
const ZoomInOut: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
@ -38,7 +39,6 @@ const ZoomInOut: FC = () => {
|
||||
fitView,
|
||||
} = useReactFlow()
|
||||
const { zoom } = useViewport()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const zoomOptions = [
|
||||
[
|
||||
@ -71,9 +71,7 @@ const ZoomInOut: FC = () => {
|
||||
],
|
||||
]
|
||||
|
||||
const handleZoom = (type: string) => {
|
||||
setOpen(false)
|
||||
|
||||
const handleZoom = (type: ZoomType) => {
|
||||
if (type === ZoomType.zoomToFit)
|
||||
fitView()
|
||||
|
||||
@ -122,11 +120,8 @@ const ZoomInOut: FC = () => {
|
||||
<span aria-hidden className="i-ri-zoom-out-line size-4 text-text-tertiary hover:text-text-secondary" />
|
||||
</button>
|
||||
</TipPopup>
|
||||
<DropdownMenu
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
>
|
||||
<DropdownMenuTrigger className={cn('flex h-8 w-[34px] items-center justify-center rounded-lg system-sm-medium text-text-tertiary hover:bg-black/5 hover:text-text-secondary', open && 'bg-black/5 text-text-secondary')}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="flex h-8 w-[34px] items-center justify-center rounded-lg system-sm-medium text-text-tertiary hover:bg-black/5 hover:text-text-secondary data-popup-open:bg-black/5 data-popup-open:text-text-secondary">
|
||||
{Number.parseFloat(`${zoom * 100}`).toFixed(0)}
|
||||
%
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
@ -67,3 +67,17 @@ export const templateDetailContract = base
|
||||
}
|
||||
}>())
|
||||
.output(type<{ data: MarketplaceTemplate }>())
|
||||
|
||||
export const downloadPluginContract = base
|
||||
.route({
|
||||
path: '/plugins/{organization}/{pluginName}/{version}/download',
|
||||
method: 'GET',
|
||||
})
|
||||
.input(type<{
|
||||
params: {
|
||||
organization: string
|
||||
pluginName: string
|
||||
version: string
|
||||
}
|
||||
}>())
|
||||
.output(type<Blob>())
|
||||
|
||||
@ -52,13 +52,14 @@ import {
|
||||
workflowDraftUpdateFeaturesContract,
|
||||
} from './console/workflow'
|
||||
import { workflowCommentContracts } from './console/workflow-comment'
|
||||
import { collectionPluginsContract, collectionsContract, searchAdvancedContract, templateDetailContract } from './marketplace'
|
||||
import { collectionPluginsContract, collectionsContract, downloadPluginContract, searchAdvancedContract, templateDetailContract } from './marketplace'
|
||||
|
||||
export const marketplaceRouterContract = {
|
||||
collections: collectionsContract,
|
||||
collectionPlugins: collectionPluginsContract,
|
||||
searchAdvanced: searchAdvancedContract,
|
||||
templateDetail: templateDetailContract,
|
||||
downloadPlugin: downloadPluginContract,
|
||||
}
|
||||
|
||||
export type MarketPlaceInputs = InferContractRouterInputs<typeof marketplaceRouterContract>
|
||||
|
||||
@ -569,15 +569,6 @@ export const usePluginManifestInfo = (pluginUID: string) => {
|
||||
})
|
||||
}
|
||||
|
||||
export const useDownloadPlugin = (info: { organization: string, pluginName: string, version: string }, needDownload: boolean) => {
|
||||
return useQuery({
|
||||
queryKey: [NAME_SPACE, 'downloadPlugin', info],
|
||||
queryFn: () => getMarketplace<Blob>(`/plugins/${info.organization}/${info.pluginName}/${info.version}/download`),
|
||||
enabled: needDownload,
|
||||
retry: 0,
|
||||
})
|
||||
}
|
||||
|
||||
export const useMutationCheckDependencies = () => {
|
||||
return useMutation({
|
||||
mutationFn: (appId: string) => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user