mirror of
https://github.com/langgenius/dify.git
synced 2026-08-02 02:40:43 +08:00
refactor(web): align Base UI state consumers (#39864)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
eec0e2e44e
commit
b6824334cf
@ -1067,14 +1067,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/base/date-and-time-picker/time-picker/__tests__/index.spec.tsx": {
|
||||
"jsx_a11y/click-events-have-key-events": {
|
||||
"count": 1
|
||||
},
|
||||
"jsx_a11y/no-static-element-interactions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/base/date-and-time-picker/time-picker/index.tsx": {
|
||||
"jsx_a11y/click-events-have-key-events": {
|
||||
"count": 1
|
||||
@ -3342,14 +3334,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/plugins/plugin-detail-panel/subscription-list/__tests__/selector-entry.spec.tsx": {
|
||||
"jsx_a11y/click-events-have-key-events": {
|
||||
"count": 1
|
||||
},
|
||||
"jsx_a11y/no-static-element-interactions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/use-common-modal-state.ts": {
|
||||
"erasable-syntax-only/enums": {
|
||||
"count": 1
|
||||
|
||||
@ -12,10 +12,19 @@ type DropdownMenuProps = {
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}
|
||||
|
||||
type DropdownMenuTriggerProps = React.HTMLAttributes<HTMLElement> & {
|
||||
type TriggerHtmlProps = React.HTMLAttributes<HTMLElement> & {
|
||||
'data-testid'?: string
|
||||
'data-disabled'?: string
|
||||
'data-popup-open'?: string
|
||||
}
|
||||
|
||||
type DropdownMenuTriggerProps = TriggerHtmlProps & {
|
||||
children?: ReactNode
|
||||
disabled?: boolean
|
||||
nativeButton?: boolean
|
||||
render?: React.ReactElement
|
||||
render?:
|
||||
| React.ReactElement
|
||||
| ((props: TriggerHtmlProps, state: { open: boolean }) => React.ReactElement)
|
||||
}
|
||||
|
||||
type DropdownMenuContentProps = React.HTMLAttributes<HTMLDivElement> & {
|
||||
@ -51,16 +60,33 @@ export const DropdownMenuTrigger = ({
|
||||
render,
|
||||
nativeButton: _nativeButton,
|
||||
onClick,
|
||||
disabled,
|
||||
...props
|
||||
}: DropdownMenuTriggerProps) => {
|
||||
const { open, onOpenChange } = React.useContext(DropdownMenuContext)
|
||||
const node = render ?? children
|
||||
const isNativeButton = React.isValidElement(node) && node.type === 'button'
|
||||
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
|
||||
if (disabled) return
|
||||
onClick?.(event)
|
||||
if (!event.defaultPrevented) onOpenChange(!open)
|
||||
}
|
||||
|
||||
if (typeof render === 'function') {
|
||||
return render(
|
||||
{
|
||||
...props,
|
||||
'aria-disabled': disabled || undefined,
|
||||
'data-testid': props['data-testid'] ?? 'dropdown-menu-trigger',
|
||||
'data-disabled': disabled ? '' : undefined,
|
||||
'data-popup-open': open ? '' : undefined,
|
||||
onClick: handleClick,
|
||||
},
|
||||
{ open },
|
||||
)
|
||||
}
|
||||
|
||||
const node = render ?? children
|
||||
const isNativeButton = React.isValidElement(node) && node.type === 'button'
|
||||
|
||||
if (React.isValidElement(node)) {
|
||||
const triggerElement = node as React.ReactElement<Record<string, unknown>>
|
||||
const childProps = (triggerElement.props ?? {}) as React.HTMLAttributes<HTMLElement> & {
|
||||
@ -80,6 +106,10 @@ export const DropdownMenuTrigger = ({
|
||||
...childProps,
|
||||
'data-testid':
|
||||
childProps['data-testid'] ?? triggerProps['data-testid'] ?? 'dropdown-menu-trigger',
|
||||
'data-disabled': disabled ? '' : undefined,
|
||||
'data-popup-open': open ? '' : undefined,
|
||||
disabled: isNativeButton ? disabled : undefined,
|
||||
'aria-disabled': !isNativeButton && disabled ? true : childProps['aria-disabled'],
|
||||
role,
|
||||
tabIndex:
|
||||
childProps.tabIndex ?? triggerProps.tabIndex ?? (role === 'button' ? 0 : undefined),
|
||||
|
||||
@ -12,10 +12,18 @@ type PopoverProps = {
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}
|
||||
|
||||
type PopoverTriggerProps = React.HTMLAttributes<HTMLElement> & {
|
||||
type TriggerHtmlProps = React.HTMLAttributes<HTMLElement> & {
|
||||
'data-testid'?: string
|
||||
'data-popover-trigger'?: string
|
||||
'data-popup-open'?: string
|
||||
}
|
||||
|
||||
type PopoverTriggerProps = TriggerHtmlProps & {
|
||||
children?: ReactNode
|
||||
nativeButton?: boolean
|
||||
render?: React.ReactElement
|
||||
render?:
|
||||
| React.ReactElement
|
||||
| ((props: TriggerHtmlProps, state: { open: boolean }) => React.ReactElement)
|
||||
}
|
||||
|
||||
type PopoverContentProps = React.HTMLAttributes<HTMLDivElement> & {
|
||||
@ -84,6 +92,21 @@ export const PopoverTrigger = ({
|
||||
...props
|
||||
}: PopoverTriggerProps) => {
|
||||
const { open, onOpenChange } = React.useContext(PopoverContext)
|
||||
if (typeof render === 'function') {
|
||||
const triggerProps: TriggerHtmlProps = {
|
||||
...props,
|
||||
'data-testid': props['data-testid'] ?? 'popover-trigger',
|
||||
'data-popover-trigger': 'true',
|
||||
'data-popup-open': open ? '' : undefined,
|
||||
onClick: (event: React.MouseEvent<HTMLElement>) => {
|
||||
onClick?.(event)
|
||||
if (event.defaultPrevented) return
|
||||
onOpenChange(!open)
|
||||
},
|
||||
}
|
||||
return render(triggerProps, { open })
|
||||
}
|
||||
|
||||
const node = render ?? children
|
||||
|
||||
if (React.isValidElement(node)) {
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
'use client'
|
||||
import type { Dayjs } from 'dayjs'
|
||||
import type { FC } from 'react'
|
||||
import type { TriggerProps } from '@/app/components/base/date-and-time-picker/types'
|
||||
import type {
|
||||
DatePickerProps,
|
||||
TriggerProps,
|
||||
} from '@/app/components/base/date-and-time-picker/types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { RiCalendarLine } from '@remixicon/react'
|
||||
import dayjs from 'dayjs'
|
||||
@ -23,15 +26,19 @@ const today = dayjs()
|
||||
const DatePicker: FC<Props> = ({ start, end, onStartChange, onEndChange }) => {
|
||||
const locale = useLocale()
|
||||
|
||||
const renderDate = useCallback(
|
||||
({ value, handleClickTrigger, isOpen }: TriggerProps) => {
|
||||
const renderDate = useCallback<NonNullable<DatePickerProps['renderTrigger']>>(
|
||||
(props, _state, { value, handleClickTrigger }: TriggerProps) => {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={cn(
|
||||
'flex h-7 cursor-pointer items-center rounded-lg px-1 system-sm-regular text-components-input-text-filled hover:bg-state-base-hover',
|
||||
isOpen && 'bg-state-base-hover',
|
||||
'flex h-7 cursor-pointer items-center rounded-lg px-1 system-sm-regular text-components-input-text-filled hover:bg-state-base-hover data-popup-open:bg-state-base-hover',
|
||||
props.className,
|
||||
)}
|
||||
onClick={handleClickTrigger}
|
||||
onClick={(event) => {
|
||||
handleClickTrigger(event)
|
||||
props.onClick?.(event)
|
||||
}}
|
||||
>
|
||||
{value ? formatToLocalTime(value, locale, 'MMM D') : ''}
|
||||
</div>
|
||||
|
||||
@ -10,7 +10,6 @@ import {
|
||||
AlertDialogDescription,
|
||||
AlertDialogTitle,
|
||||
} from '@langgenius/dify-ui/alert-dialog'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@ -127,12 +126,7 @@ const SnippetInfoDropdown = ({ snippet }: SnippetInfoDropdownProps) => {
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu open={open} onOpenChange={setOpen}>
|
||||
<DropdownMenuTrigger
|
||||
className={cn(
|
||||
'action-btn action-btn-m size-6 rounded-md text-text-tertiary',
|
||||
open && 'bg-state-base-hover text-text-secondary',
|
||||
)}
|
||||
>
|
||||
<DropdownMenuTrigger className="action-btn action-btn-m size-6 rounded-md text-text-tertiary data-popup-open:bg-state-base-hover data-popup-open:text-text-secondary">
|
||||
<span aria-hidden className="i-ri-more-fill size-4" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent placement="bottom-end" sideOffset={4} popupClassName="w-[180px] p-1">
|
||||
|
||||
@ -61,7 +61,7 @@ describe('AccessControlItem', () => {
|
||||
|
||||
const organization = screen.getByRole('radio', { name: 'Organization Only' })
|
||||
expect(organization).toHaveAttribute('aria-disabled', 'true')
|
||||
expect(organization).toHaveClass('cursor-not-allowed')
|
||||
expect(organization).toHaveAttribute('data-disabled')
|
||||
|
||||
await user.click(organization)
|
||||
|
||||
|
||||
@ -283,12 +283,18 @@ describe('AddMemberOrGroupDialog', () => {
|
||||
expect(document.querySelector('.spin-animation')).toBeInTheDocument()
|
||||
|
||||
const groupOption = screen.getByRole('option', { name: /Group One/ })
|
||||
expect(groupOption).not.toHaveAttribute('data-selected')
|
||||
fireEvent.click(groupOption)
|
||||
expect(groupOption).toHaveAttribute('data-selected')
|
||||
fireEvent.click(groupOption)
|
||||
expect(groupOption).not.toHaveAttribute('data-selected')
|
||||
|
||||
const memberOption = screen.getByRole('option', { name: /Member One/ })
|
||||
expect(memberOption).not.toHaveAttribute('data-selected')
|
||||
fireEvent.click(memberOption)
|
||||
expect(memberOption).toHaveAttribute('data-selected')
|
||||
fireEvent.click(memberOption)
|
||||
expect(memberOption).not.toHaveAttribute('data-selected')
|
||||
|
||||
fireEvent.click(screen.getByText('app.accessControlDialog.operateGroupAndMember.expand'))
|
||||
fireEvent.click(screen.getByText('app.accessControlDialog.operateGroupAndMember.allMembers'))
|
||||
|
||||
@ -19,9 +19,7 @@ export default function AccessControlItem({ type, children, disabled }: AccessCo
|
||||
'rounded-[10px] border-[0.5px] border-components-option-card-option-border bg-components-option-card-option-bg shadow-xs transition-colors',
|
||||
'focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden',
|
||||
'data-checked:border-components-option-card-option-selected-border data-checked:bg-components-option-card-option-selected-bg data-checked:inset-ring-[0.5px] data-checked:inset-ring-components-option-card-option-selected-border',
|
||||
disabled
|
||||
? 'cursor-not-allowed opacity-60'
|
||||
: 'cursor-pointer hover:border-components-option-card-option-border-hover hover:bg-components-option-card-option-bg-hover',
|
||||
'cursor-pointer hover:border-components-option-card-option-border-hover hover:bg-components-option-card-option-bg-hover data-disabled:cursor-not-allowed data-disabled:opacity-60 data-disabled:hover:border-components-option-card-option-border data-disabled:hover:bg-components-option-card-option-bg',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
||||
@ -297,19 +297,23 @@ function GroupItem({ group, subject }: GroupItemProps) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 rounded-lg hover:bg-state-base-hover">
|
||||
<BaseItem subject={subject}>
|
||||
<SelectionBox checked={isChecked} />
|
||||
<ComboboxItemText className="flex grow items-center px-0">
|
||||
<div className="mr-2 size-5 overflow-hidden rounded-full bg-components-icon-bg-blue-solid">
|
||||
<div className="bg-access-app-icon-mask-bg flex size-full items-center justify-center">
|
||||
<RiOrganizationChart
|
||||
className="h-3.5 w-3.5 text-components-avatar-shape-fill-stop-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<span className="mr-1 system-sm-medium text-text-secondary">{group.name}</span>
|
||||
<span className="system-xs-regular text-text-tertiary">{group.groupSize}</span>
|
||||
</ComboboxItemText>
|
||||
{(selected) => (
|
||||
<>
|
||||
<SelectionBox checked={selected} />
|
||||
<ComboboxItemText className="flex grow items-center px-0">
|
||||
<div className="mr-2 size-5 overflow-hidden rounded-full bg-components-icon-bg-blue-solid">
|
||||
<div className="bg-access-app-icon-mask-bg flex size-full items-center justify-center">
|
||||
<RiOrganizationChart
|
||||
className="h-3.5 w-3.5 text-components-avatar-shape-fill-stop-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<span className="mr-1 system-sm-medium text-text-secondary">{group.name}</span>
|
||||
<span className="system-xs-regular text-text-tertiary">{group.groupSize}</span>
|
||||
</ComboboxItemText>
|
||||
</>
|
||||
)}
|
||||
</BaseItem>
|
||||
<Button
|
||||
size="small"
|
||||
@ -335,25 +339,27 @@ type MemberItemProps = {
|
||||
function MemberItem({ member, subject }: MemberItemProps) {
|
||||
const currentUser = useAtomValue(userProfileAtom)
|
||||
const { t } = useTranslation()
|
||||
const specificMembers = useAccessControlStore((s) => s.specificMembers)
|
||||
const isChecked = specificMembers.some((m) => m.id === member.id)
|
||||
return (
|
||||
<BaseItem subject={subject} className="pr-3">
|
||||
<SelectionBox checked={isChecked} />
|
||||
<ComboboxItemText className="flex grow items-center px-0">
|
||||
<div className="mr-2 size-5 overflow-hidden rounded-full bg-components-icon-bg-blue-solid">
|
||||
<div className="bg-access-app-icon-mask-bg flex size-full items-center justify-center">
|
||||
<Avatar size="xxs" avatar={null} name={member.name} />
|
||||
</div>
|
||||
</div>
|
||||
<span className="mr-1 system-sm-medium text-text-secondary">{member.name}</span>
|
||||
{currentUser.email === member.email && (
|
||||
<span className="system-xs-regular text-text-tertiary">
|
||||
({t(($) => $.you, { ns: 'common' })})
|
||||
</span>
|
||||
)}
|
||||
</ComboboxItemText>
|
||||
<span className="system-xs-regular text-text-quaternary">{member.email}</span>
|
||||
{(selected) => (
|
||||
<>
|
||||
<SelectionBox checked={selected} />
|
||||
<ComboboxItemText className="flex grow items-center px-0">
|
||||
<div className="mr-2 size-5 overflow-hidden rounded-full bg-components-icon-bg-blue-solid">
|
||||
<div className="bg-access-app-icon-mask-bg flex size-full items-center justify-center">
|
||||
<Avatar size="xxs" avatar={null} name={member.name} />
|
||||
</div>
|
||||
</div>
|
||||
<span className="mr-1 system-sm-medium text-text-secondary">{member.name}</span>
|
||||
{currentUser.email === member.email && (
|
||||
<span className="system-xs-regular text-text-tertiary">
|
||||
({t(($) => $.you, { ns: 'common' })})
|
||||
</span>
|
||||
)}
|
||||
</ComboboxItemText>
|
||||
<span className="system-xs-regular text-text-quaternary">{member.email}</span>
|
||||
</>
|
||||
)}
|
||||
</BaseItem>
|
||||
)
|
||||
}
|
||||
@ -361,7 +367,7 @@ function MemberItem({ member, subject }: MemberItemProps) {
|
||||
type BaseItemProps = {
|
||||
className?: string
|
||||
subject: Subject
|
||||
children: React.ReactNode
|
||||
children: (selected: boolean) => React.ReactNode
|
||||
}
|
||||
function BaseItem({ children, className, subject }: BaseItemProps) {
|
||||
return (
|
||||
@ -371,9 +377,12 @@ function BaseItem({ children, className, subject }: BaseItemProps) {
|
||||
'mx-0 flex min-h-8 grow grid-cols-none items-center gap-2 rounded-lg p-1 pl-2',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</ComboboxItem>
|
||||
render={(props, state) => (
|
||||
<div {...props} className={props.className}>
|
||||
{children(state.selected)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@ -30,12 +29,7 @@ const VersionSelector: React.FC<VersionSelectorProps> = ({ versionLen, value, on
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
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',
|
||||
)}
|
||||
className="flex cursor-pointer items-center border-none bg-transparent p-0 system-xs-medium text-text-tertiary data-disabled:cursor-default data-popup-open:text-text-secondary"
|
||||
>
|
||||
<div>
|
||||
{t(($) => $['generate.version'], { ns: 'appDebug' })} {value + 1}
|
||||
|
||||
@ -651,8 +651,7 @@ export function AppCardActionBar({ app, onRefresh }: AppCardActionBarProps) {
|
||||
}
|
||||
disabled={isExporting}
|
||||
className={cn(
|
||||
'flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden disabled:cursor-not-allowed',
|
||||
isOperationsMenuOpen ? 'bg-state-base-hover' : 'hover:bg-state-base-hover',
|
||||
'flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden disabled:cursor-not-allowed data-popup-open:bg-state-base-hover',
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
@ -1291,10 +1290,7 @@ export function AppCard({
|
||||
})
|
||||
}
|
||||
disabled={isExporting}
|
||||
className={cn(
|
||||
'flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden disabled:cursor-not-allowed',
|
||||
isOperationsMenuOpen ? 'bg-state-base-hover' : 'hover:bg-state-base-hover',
|
||||
)}
|
||||
className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden disabled:cursor-not-allowed data-popup-open:bg-state-base-hover"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
|
||||
@ -29,15 +29,16 @@ const MobileOperationDropdown = ({
|
||||
return (
|
||||
<DropdownMenu open={open} onOpenChange={setOpen}>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
render={(props, state) => (
|
||||
<ActionButton
|
||||
{...props}
|
||||
aria-label={t(($) => $['operation.more'], { ns: 'common' })}
|
||||
size="l"
|
||||
state={open ? ActionButtonState.Hover : ActionButtonState.Default}
|
||||
state={state.open ? ActionButtonState.Hover : ActionButtonState.Default}
|
||||
>
|
||||
<div className="i-ri-more-fill h-4.5 w-4.5" aria-hidden="true" />
|
||||
</ActionButton>
|
||||
}
|
||||
)}
|
||||
/>
|
||||
<DropdownMenuContent placement="bottom-end" sideOffset={4} popupClassName="min-w-[160px]">
|
||||
<DropdownMenuItem
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import { RiChatSettingsLine } from '@remixicon/react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
|
||||
import InputsFormContent from '@/app/components/base/chat/chat-with-history/inputs-form/content'
|
||||
@ -8,16 +7,18 @@ import { Message3Fill } from '@/app/components/base/icons/src/public/other'
|
||||
|
||||
const ViewFormDropdown = () => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<Popover>
|
||||
<PopoverTrigger
|
||||
render={
|
||||
<ActionButton size="l" state={open ? ActionButtonState.Hover : ActionButtonState.Default}>
|
||||
render={(props, state) => (
|
||||
<ActionButton
|
||||
{...props}
|
||||
size="l"
|
||||
state={state.open ? ActionButtonState.Hover : ActionButtonState.Default}
|
||||
>
|
||||
<RiChatSettingsLine className="h-4.5 w-4.5" />
|
||||
</ActionButton>
|
||||
}
|
||||
)}
|
||||
/>
|
||||
<PopoverContent
|
||||
placement="bottom-end"
|
||||
|
||||
@ -48,24 +48,26 @@ const Operation: FC<Props> = ({
|
||||
return (
|
||||
<DropdownMenu modal={false} open={open} onOpenChange={setOpen}>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
render={(props, state) => (
|
||||
<ActionButton
|
||||
{...props}
|
||||
className={cn(
|
||||
isItemHovering || open
|
||||
isItemHovering || state.open
|
||||
? 'pointer-events-auto opacity-100'
|
||||
: 'pointer-events-none opacity-0',
|
||||
props.className,
|
||||
)}
|
||||
state={
|
||||
isActive
|
||||
? ActionButtonState.Active
|
||||
: open
|
||||
: state.open
|
||||
? ActionButtonState.Hover
|
||||
: ActionButtonState.Default
|
||||
}
|
||||
>
|
||||
<span aria-hidden className="i-ri-more-fill size-4" />
|
||||
</ActionButton>
|
||||
}
|
||||
)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
<DropdownMenuContent
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
|
||||
import InputsFormContent from '@/app/components/base/chat/embedded-chatbot/inputs-form/content'
|
||||
@ -12,20 +11,19 @@ type Props = Readonly<{
|
||||
|
||||
const ViewFormDropdown = ({ iconColor }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<Popover>
|
||||
<PopoverTrigger
|
||||
render={
|
||||
render={(props, state) => (
|
||||
<ActionButton
|
||||
{...props}
|
||||
size="l"
|
||||
state={open ? ActionButtonState.Hover : ActionButtonState.Default}
|
||||
state={state.open ? ActionButtonState.Hover : ActionButtonState.Default}
|
||||
data-testid="view-form-dropdown-trigger"
|
||||
>
|
||||
<div className={cn('i-ri-chat-settings-line h-4.5 w-4.5 shrink-0', iconColor)} />
|
||||
</ActionButton>
|
||||
}
|
||||
)}
|
||||
/>
|
||||
<PopoverContent
|
||||
placement="bottom-end"
|
||||
|
||||
@ -400,8 +400,16 @@ describe('DatePicker', () => {
|
||||
describe('Clear Behavior', () => {
|
||||
it('should call onClear when clear is clicked while picker is closed', () => {
|
||||
const onClear = vi.fn()
|
||||
const renderTrigger = vi.fn(({ handleClear }) => (
|
||||
<button data-testid="clear-trigger" onClick={handleClear}>
|
||||
const renderTrigger = vi.fn((triggerProps, _state, { handleClear }) => (
|
||||
<button
|
||||
{...triggerProps}
|
||||
data-testid="clear-trigger"
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
handleClear(event)
|
||||
triggerProps.onClick?.(event)
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
))
|
||||
@ -420,8 +428,8 @@ describe('DatePicker', () => {
|
||||
it('should clear selected date without calling onClear when picker is open', () => {
|
||||
const onClear = vi.fn()
|
||||
const onChange = vi.fn()
|
||||
const renderTrigger = vi.fn(({ handleClickTrigger, handleClear }) => (
|
||||
<div>
|
||||
const renderTrigger = vi.fn((triggerProps, _state, { handleClickTrigger, handleClear }) => (
|
||||
<div {...triggerProps}>
|
||||
<button data-testid="open-trigger" onClick={handleClickTrigger}>
|
||||
Open
|
||||
</button>
|
||||
@ -576,8 +584,8 @@ describe('DatePicker', () => {
|
||||
// Custom trigger
|
||||
describe('Custom Trigger', () => {
|
||||
it('should use renderTrigger when provided', () => {
|
||||
const renderTrigger = vi.fn(({ handleClickTrigger }) => (
|
||||
<button data-testid="custom-trigger" onClick={handleClickTrigger}>
|
||||
const renderTrigger = vi.fn((triggerProps, _state, { handleClickTrigger }) => (
|
||||
<button {...triggerProps} data-testid="custom-trigger" onClick={handleClickTrigger}>
|
||||
Custom
|
||||
</button>
|
||||
))
|
||||
@ -589,8 +597,8 @@ describe('DatePicker', () => {
|
||||
})
|
||||
|
||||
it('should open picker when custom trigger is clicked', () => {
|
||||
const renderTrigger = vi.fn(({ handleClickTrigger }) => (
|
||||
<button data-testid="custom-trigger" onClick={handleClickTrigger}>
|
||||
const renderTrigger = vi.fn((triggerProps, _state, { handleClickTrigger }) => (
|
||||
<button {...triggerProps} data-testid="custom-trigger" onClick={handleClickTrigger}>
|
||||
Custom
|
||||
</button>
|
||||
))
|
||||
@ -602,6 +610,24 @@ describe('DatePicker', () => {
|
||||
|
||||
expect(screen.getAllByText(/daysInWeek/).length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should expose Base UI trigger state and props to a custom trigger', () => {
|
||||
const renderTrigger = vi.fn((triggerProps, state) => (
|
||||
<button {...triggerProps} data-testid="state-trigger">
|
||||
{state.open ? 'Open' : 'Closed'}
|
||||
</button>
|
||||
))
|
||||
|
||||
render(<DatePicker {...createDatePickerProps({ renderTrigger })} />)
|
||||
|
||||
expect(screen.getByTestId('state-trigger')).toHaveTextContent('Closed')
|
||||
expect(screen.getByTestId('state-trigger')).not.toHaveAttribute('data-popup-open')
|
||||
|
||||
fireEvent.click(screen.getByTestId('state-trigger'))
|
||||
|
||||
expect(screen.getByTestId('state-trigger')).toHaveTextContent('Open')
|
||||
expect(screen.getByTestId('state-trigger')).toHaveAttribute('data-popup-open')
|
||||
})
|
||||
})
|
||||
|
||||
// Disabled dates
|
||||
|
||||
@ -223,42 +223,50 @@ const DatePicker = ({
|
||||
: t(($) => $['dateFormats.display'], { ns: 'time' })
|
||||
const displayValue = normalizedValue?.format(timeFormat) || ''
|
||||
const displayTime = selectedDate?.format('hh:mm A') || '--:-- --'
|
||||
const placeholderDate =
|
||||
isOpen && selectedDate
|
||||
? selectedDate.format(timeFormat)
|
||||
: placeholder || t(($) => $.defaultPlaceholder, { ns: 'time' })
|
||||
|
||||
return (
|
||||
<Popover open={isOpen} onOpenChange={handleOpenChange}>
|
||||
<PopoverTrigger
|
||||
nativeButton={false}
|
||||
className={triggerWrapClassName}
|
||||
render={
|
||||
renderTrigger ? (
|
||||
renderTrigger({
|
||||
render={(props, state) => {
|
||||
if (renderTrigger) {
|
||||
return renderTrigger(props, state, {
|
||||
value: normalizedValue,
|
||||
selectedDate,
|
||||
isOpen,
|
||||
handleClear,
|
||||
handleClickTrigger,
|
||||
})
|
||||
) : (
|
||||
}
|
||||
|
||||
const placeholderDate =
|
||||
state.open && selectedDate
|
||||
? selectedDate.format(timeFormat)
|
||||
: placeholder || t(($) => $.defaultPlaceholder, { ns: 'time' })
|
||||
|
||||
return (
|
||||
<div
|
||||
className="group flex w-63 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"
|
||||
onClick={handleClickTrigger}
|
||||
{...props}
|
||||
className={cn(
|
||||
'group flex w-63 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',
|
||||
props.className,
|
||||
)}
|
||||
onClick={(event) => {
|
||||
handleClickTrigger(event)
|
||||
props.onClick?.(event)
|
||||
}}
|
||||
data-testid="date-picker-trigger"
|
||||
>
|
||||
<input
|
||||
className="flex-1 cursor-pointer appearance-none truncate bg-transparent p-1 system-xs-regular text-components-input-text-filled outline-hidden placeholder:text-components-input-text-placeholder"
|
||||
readOnly
|
||||
value={isOpen ? '' : displayValue}
|
||||
value={state.open ? '' : displayValue}
|
||||
placeholder={placeholderDate}
|
||||
/>
|
||||
<span
|
||||
className={cn(
|
||||
'i-ri-calendar-line size-4 shrink-0 text-text-quaternary',
|
||||
isOpen ? 'text-text-secondary' : 'group-hover:text-text-secondary',
|
||||
(displayValue || (isOpen && selectedDate)) && 'group-hover:hidden',
|
||||
state.open ? 'text-text-secondary' : 'group-hover:text-text-secondary',
|
||||
(displayValue || (state.open && selectedDate)) && 'group-hover:hidden',
|
||||
)}
|
||||
/>
|
||||
<button
|
||||
@ -266,7 +274,7 @@ const DatePicker = ({
|
||||
aria-label={t(($) => $['operation.clear'], { ns: 'common' })}
|
||||
className={cn(
|
||||
'hidden size-4 shrink-0 border-none bg-transparent p-0 text-text-quaternary hover:text-text-secondary focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden',
|
||||
(displayValue || (isOpen && selectedDate)) && 'group-hover:inline-block',
|
||||
(displayValue || (state.open && selectedDate)) && 'group-hover:inline-block',
|
||||
)}
|
||||
onClick={handleClear}
|
||||
>
|
||||
@ -274,7 +282,7 @@ const DatePicker = ({
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<PopoverContent
|
||||
placement="bottom-end"
|
||||
|
||||
@ -212,8 +212,8 @@ describe('TimePicker', () => {
|
||||
})
|
||||
|
||||
it('should use renderTrigger when provided', () => {
|
||||
const renderTrigger = vi.fn(({ inputElem, onClick }) => (
|
||||
<div data-testid="custom-trigger" onClick={onClick}>
|
||||
const renderTrigger = vi.fn((triggerProps, _state, { inputElem }) => (
|
||||
<div {...triggerProps} data-testid="custom-trigger">
|
||||
{inputElem}
|
||||
</div>
|
||||
))
|
||||
@ -224,6 +224,25 @@ describe('TimePicker', () => {
|
||||
expect(renderTrigger).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should expose Base UI trigger state and props to a custom trigger', () => {
|
||||
const renderTrigger = vi.fn((triggerProps, state, { inputElem }) => (
|
||||
<button {...triggerProps} data-testid="state-trigger">
|
||||
{state.open ? 'Open' : 'Closed'}
|
||||
{inputElem}
|
||||
</button>
|
||||
))
|
||||
|
||||
render(<TimePicker {...baseProps} renderTrigger={renderTrigger} />)
|
||||
|
||||
expect(screen.getByTestId('state-trigger')).toHaveTextContent('Closed')
|
||||
expect(screen.getByTestId('state-trigger')).not.toHaveAttribute('data-popup-open')
|
||||
|
||||
fireEvent.click(screen.getByTestId('state-trigger'))
|
||||
|
||||
expect(screen.getByTestId('state-trigger')).toHaveTextContent('Open')
|
||||
expect(screen.getByTestId('state-trigger')).toHaveAttribute('data-popup-open')
|
||||
})
|
||||
|
||||
it('should render with notClearable prop without errors', () => {
|
||||
render(<TimePicker {...baseProps} notClearable={true} value="10:00 AM" timezone="UTC" />)
|
||||
|
||||
|
||||
@ -194,39 +194,43 @@ const TimePicker = ({
|
||||
)
|
||||
|
||||
const displayValue = formatTimeValue(value)
|
||||
const renderInput = (open: boolean) => {
|
||||
const placeholderDate =
|
||||
open && isDayjsObject(selectedTime)
|
||||
? selectedTime.format(timeFormat)
|
||||
: placeholder || t(($) => $.defaultPlaceholder, { ns: 'time' })
|
||||
|
||||
const placeholderDate =
|
||||
isOpen && isDayjsObject(selectedTime)
|
||||
? selectedTime.format(timeFormat)
|
||||
: placeholder || t(($) => $.defaultPlaceholder, { ns: 'time' })
|
||||
|
||||
const inputElem = (
|
||||
<input
|
||||
className="flex-1 cursor-pointer appearance-none truncate bg-transparent p-1 system-xs-regular text-components-input-text-filled outline-hidden select-none placeholder:text-components-input-text-placeholder"
|
||||
readOnly
|
||||
value={isOpen ? '' : displayValue}
|
||||
placeholder={placeholderDate}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<input
|
||||
className="flex-1 cursor-pointer appearance-none truncate bg-transparent p-1 system-xs-regular text-components-input-text-filled outline-hidden select-none placeholder:text-components-input-text-placeholder"
|
||||
readOnly
|
||||
value={open ? '' : displayValue}
|
||||
placeholder={placeholderDate}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Popover open={isOpen} onOpenChange={handleOpenChange}>
|
||||
<PopoverTrigger
|
||||
nativeButton={false}
|
||||
className={triggerFullWidth ? 'flex! w-full' : undefined}
|
||||
render={
|
||||
renderTrigger ? (
|
||||
renderTrigger({
|
||||
inputElem,
|
||||
onClick: handleClickTrigger,
|
||||
isOpen,
|
||||
})
|
||||
) : (
|
||||
render={(props, state) => {
|
||||
const inputElem = renderInput(state.open)
|
||||
if (renderTrigger)
|
||||
return renderTrigger(props, state, { inputElem, onClick: handleClickTrigger })
|
||||
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
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',
|
||||
triggerFullWidth ? 'w-full min-w-0' : 'w-63',
|
||||
props.className,
|
||||
)}
|
||||
onClick={handleClickTrigger}
|
||||
onClick={(event) => {
|
||||
handleClickTrigger(event)
|
||||
props.onClick?.(event)
|
||||
}}
|
||||
data-testid="time-picker-trigger"
|
||||
>
|
||||
{inputElem}
|
||||
@ -240,8 +244,8 @@ const TimePicker = ({
|
||||
<span
|
||||
className={cn(
|
||||
'i-ri-time-line size-4 shrink-0 text-text-quaternary',
|
||||
isOpen ? 'text-text-secondary' : 'group-hover:text-text-secondary',
|
||||
(displayValue || (isOpen && selectedTime)) &&
|
||||
state.open ? 'text-text-secondary' : 'group-hover:text-text-secondary',
|
||||
(displayValue || (state.open && selectedTime)) &&
|
||||
!notClearable &&
|
||||
'group-hover:hidden',
|
||||
)}
|
||||
@ -250,7 +254,7 @@ const TimePicker = ({
|
||||
type="button"
|
||||
className={cn(
|
||||
'hidden size-4 shrink-0 border-none bg-transparent p-0 text-text-quaternary hover:text-text-secondary focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden',
|
||||
(displayValue || (isOpen && selectedTime)) &&
|
||||
(displayValue || (state.open && selectedTime)) &&
|
||||
!notClearable &&
|
||||
'group-hover:inline-block',
|
||||
)}
|
||||
@ -261,7 +265,7 @@ const TimePicker = ({
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<PopoverContent
|
||||
placement={placement}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { Placement } from '@langgenius/dify-ui/popover'
|
||||
import type { Placement, PopoverTriggerProps } from '@langgenius/dify-ui/popover'
|
||||
import type { Dayjs } from 'dayjs'
|
||||
|
||||
export enum ViewType {
|
||||
@ -12,10 +12,13 @@ export enum Period {
|
||||
PM = 'PM',
|
||||
}
|
||||
|
||||
type PopoverTriggerRender = Exclude<NonNullable<PopoverTriggerProps['render']>, React.ReactElement>
|
||||
type TriggerRenderProps = Parameters<PopoverTriggerRender>[0]
|
||||
type TriggerState = Parameters<PopoverTriggerRender>[1]
|
||||
|
||||
export type TriggerProps = {
|
||||
value: Dayjs | undefined
|
||||
selectedDate: Dayjs | undefined
|
||||
isOpen: boolean
|
||||
handleClear: (e: React.MouseEvent) => void
|
||||
handleClickTrigger: (e: React.MouseEvent) => void
|
||||
}
|
||||
@ -28,7 +31,11 @@ export type DatePickerProps = {
|
||||
onChange: (date: Dayjs | undefined) => void
|
||||
onClear: () => void
|
||||
triggerWrapClassName?: string
|
||||
renderTrigger?: (props: TriggerProps) => React.ReactElement
|
||||
renderTrigger?: (
|
||||
props: TriggerRenderProps,
|
||||
state: TriggerState,
|
||||
params: TriggerProps,
|
||||
) => React.ReactElement
|
||||
minuteFilter?: (minutes: string[]) => string[]
|
||||
noConfirm?: boolean
|
||||
getIsDateDisabled?: (date: Dayjs) => boolean
|
||||
@ -51,7 +58,6 @@ export type DatePickerFooterProps = {
|
||||
}
|
||||
|
||||
export type TriggerParams = {
|
||||
isOpen: boolean
|
||||
inputElem: React.ReactNode
|
||||
onClick: (e: React.MouseEvent) => void
|
||||
}
|
||||
@ -61,7 +67,11 @@ export type TimePickerProps = {
|
||||
placeholder?: string
|
||||
onChange: (date: Dayjs | undefined) => void
|
||||
onClear: () => void
|
||||
renderTrigger?: (props: TriggerParams) => React.ReactElement
|
||||
renderTrigger?: (
|
||||
props: TriggerRenderProps,
|
||||
state: TriggerState,
|
||||
params: TriggerParams,
|
||||
) => React.ReactElement
|
||||
title?: string
|
||||
minuteFilter?: (minutes: string[]) => string[]
|
||||
popupClassName?: string
|
||||
|
||||
@ -148,6 +148,26 @@ describe('FollowUpSettingModal', () => {
|
||||
})
|
||||
|
||||
describe('Custom Prompt', () => {
|
||||
it('should expose the selected prompt mode through RadioItem state attributes', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderModal()
|
||||
|
||||
const defaultOption = screen
|
||||
.getByText('appDebug.feature.suggestedQuestionsAfterAnswer.modal.defaultPromptOption')
|
||||
.closest('button')!
|
||||
const customOption = screen
|
||||
.getByText('appDebug.feature.suggestedQuestionsAfterAnswer.modal.customPromptOption')
|
||||
.closest('button')!
|
||||
|
||||
expect(defaultOption).toHaveAttribute('data-checked')
|
||||
expect(customOption).not.toHaveAttribute('data-checked')
|
||||
|
||||
await user.click(customOption)
|
||||
|
||||
expect(defaultOption).not.toHaveAttribute('data-checked')
|
||||
expect(customOption).toHaveAttribute('data-checked')
|
||||
})
|
||||
|
||||
it('should enable custom prompt input and save the custom prompt when selected', async () => {
|
||||
const user = userEvent.setup()
|
||||
const { onSave } = renderModal()
|
||||
|
||||
@ -2,7 +2,6 @@ import type { SuggestedQuestionsAfterAnswer } from '@/app/components/base/featur
|
||||
import type { FormValue } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import type { CompletionParams, Model, ModelModeType } from '@/types/app'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
|
||||
import { Field, FieldItem } from '@langgenius/dify-ui/field'
|
||||
import { Fieldset, FieldsetLegend } from '@langgenius/dify-ui/fieldset'
|
||||
@ -158,12 +157,7 @@ const FollowUpSettingModal = ({ data, onSave, onCancel }: FollowUpSettingModalPr
|
||||
value={PROMPT_MODE.default}
|
||||
nativeButton
|
||||
render={<button type="button" />}
|
||||
className={cn(
|
||||
'w-full rounded-xl border p-4 text-left transition-colors',
|
||||
promptMode === PROMPT_MODE.default
|
||||
? 'border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg'
|
||||
: 'border-components-option-card-option-border bg-components-option-card-option-bg hover:bg-state-base-hover',
|
||||
)}
|
||||
className="w-full rounded-xl border border-components-option-card-option-border bg-components-option-card-option-bg p-4 text-left transition-colors hover:bg-state-base-hover data-checked:border-components-option-card-option-selected-border data-checked:bg-components-option-card-option-selected-bg data-checked:hover:bg-components-option-card-option-selected-bg"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
@ -200,12 +194,7 @@ const FollowUpSettingModal = ({ data, onSave, onCancel }: FollowUpSettingModalPr
|
||||
value={PROMPT_MODE.custom}
|
||||
nativeButton
|
||||
render={<button type="button" />}
|
||||
className={cn(
|
||||
'w-full rounded-xl border p-4 text-left transition-colors',
|
||||
promptMode === PROMPT_MODE.custom
|
||||
? 'border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg'
|
||||
: 'border-components-option-card-option-border bg-components-option-card-option-bg hover:bg-state-base-hover',
|
||||
)}
|
||||
className="w-full rounded-xl border border-components-option-card-option-border bg-components-option-card-option-bg p-4 text-left transition-colors hover:bg-state-base-hover data-checked:border-components-option-card-option-selected-border data-checked:bg-components-option-card-option-selected-bg data-checked:hover:bg-components-option-card-option-selected-bg"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
|
||||
@ -37,7 +37,11 @@ const createFileConfig = (overrides: Partial<FileUpload> = {}): FileUpload =>
|
||||
function renderAndOpen(props: Partial<React.ComponentProps<typeof FileFromLinkOrLocal>> = {}) {
|
||||
const trigger =
|
||||
props.trigger ??
|
||||
((open: boolean) => <button data-testid="trigger">{open ? 'Close' : 'Open'}</button>)
|
||||
((triggerProps, state) => (
|
||||
<button {...triggerProps} data-testid="trigger">
|
||||
{state.open ? 'Close' : 'Open'}
|
||||
</button>
|
||||
))
|
||||
const result = render(
|
||||
<FileContextProvider value={mockFiles}>
|
||||
<FileFromLinkOrLocal
|
||||
@ -58,10 +62,13 @@ describe('FileFromLinkOrLocal', () => {
|
||||
})
|
||||
|
||||
it('should render trigger element', () => {
|
||||
const trigger = (open: boolean) => (
|
||||
<button data-testid="trigger">
|
||||
const trigger: NonNullable<React.ComponentProps<typeof FileFromLinkOrLocal>['trigger']> = (
|
||||
triggerProps,
|
||||
state,
|
||||
) => (
|
||||
<button {...triggerProps} data-testid="trigger">
|
||||
Open
|
||||
{open ? 'close' : 'open'}
|
||||
{state.open ? 'close' : 'open'}
|
||||
</button>
|
||||
)
|
||||
render(
|
||||
@ -173,8 +180,13 @@ describe('FileFromLinkOrLocal', () => {
|
||||
})
|
||||
|
||||
it('should toggle open state when trigger is clicked', () => {
|
||||
const trigger = (open: boolean) => (
|
||||
<button data-testid="trigger">{open ? 'Close' : 'Open'}</button>
|
||||
const trigger: NonNullable<React.ComponentProps<typeof FileFromLinkOrLocal>['trigger']> = (
|
||||
triggerProps,
|
||||
state,
|
||||
) => (
|
||||
<button {...triggerProps} data-testid="trigger">
|
||||
{state.open ? 'Close' : 'Open'}
|
||||
</button>
|
||||
)
|
||||
render(
|
||||
<FileContextProvider value={mockFiles}>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import type { PopoverTriggerProps } from '@langgenius/dify-ui/popover'
|
||||
import type { FileUpload } from '@/app/components/base/features/types'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
@ -13,7 +14,7 @@ import { useStore } from '../store'
|
||||
type FileFromLinkOrLocalProps = {
|
||||
showFromLink?: boolean
|
||||
showFromLocal?: boolean
|
||||
trigger: (open: boolean) => React.ReactElement
|
||||
trigger: NonNullable<PopoverTriggerProps['render']>
|
||||
fileConfig: FileUpload
|
||||
}
|
||||
const FileFromLinkOrLocal = ({
|
||||
@ -24,7 +25,6 @@ const FileFromLinkOrLocal = ({
|
||||
}: FileFromLinkOrLocalProps) => {
|
||||
const { t } = useTranslation()
|
||||
const files = useStore((s) => s.files)
|
||||
const [open, setOpen] = useState(false)
|
||||
const [url, setUrl] = useState('')
|
||||
const [showError, setShowError] = useState(false)
|
||||
const { handleLoadFileFromLink } = useFile(fileConfig)
|
||||
@ -48,8 +48,8 @@ const FileFromLinkOrLocal = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger render={trigger(open)} />
|
||||
<Popover>
|
||||
<PopoverTrigger render={trigger} />
|
||||
<PopoverContent
|
||||
placement="top"
|
||||
sideOffset={4}
|
||||
|
||||
@ -39,13 +39,13 @@ const FileUploaderInAttachment = ({ isDisabled, fileConfig }: FileUploaderInAtta
|
||||
]
|
||||
|
||||
const renderButton = useCallback(
|
||||
(option: Option, open?: boolean) => {
|
||||
(option: Option) => {
|
||||
return (
|
||||
<Button
|
||||
variant="tertiary"
|
||||
className={cn(
|
||||
'relative w-full min-w-0',
|
||||
open && 'bg-components-button-tertiary-bg-hover',
|
||||
'data-popup-open:bg-components-button-tertiary-bg-hover',
|
||||
)}
|
||||
disabled={!!(fileConfig.number_limits && files.length >= fileConfig.number_limits)}
|
||||
>
|
||||
@ -57,12 +57,6 @@ const FileUploaderInAttachment = ({ isDisabled, fileConfig }: FileUploaderInAtta
|
||||
},
|
||||
[fileConfig, files.length],
|
||||
)
|
||||
const renderTrigger = useCallback(
|
||||
(option: Option) => {
|
||||
return (open: boolean) => renderButton(option, open)
|
||||
},
|
||||
[renderButton],
|
||||
)
|
||||
const renderOption = useCallback(
|
||||
(option: Option) => {
|
||||
if (
|
||||
@ -84,14 +78,14 @@ const FileUploaderInAttachment = ({ isDisabled, fileConfig }: FileUploaderInAtta
|
||||
<div key={option.value} className="min-w-0 flex-1">
|
||||
<FileFromLinkOrLocal
|
||||
showFromLocal={false}
|
||||
trigger={renderTrigger(option)}
|
||||
trigger={renderButton(option)}
|
||||
fileConfig={fileConfig}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
[renderButton, renderTrigger, fileConfig],
|
||||
[renderButton, fileConfig],
|
||||
)
|
||||
|
||||
return (
|
||||
|
||||
@ -11,35 +11,32 @@ type FileUploaderInChatInputProps = {
|
||||
}
|
||||
const FileUploaderInChatInput = ({ fileConfig, readonly }: FileUploaderInChatInputProps) => {
|
||||
const { t } = useTranslation()
|
||||
const renderTrigger = useCallback(
|
||||
(_open: boolean) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t(($) => $['fileUploader.uploadFromComputer'], { ns: 'common' })}
|
||||
className={cn(
|
||||
'inline-flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-lg p-1.5 text-text-tertiary outline-hidden',
|
||||
'hover:bg-state-base-hover hover:text-text-secondary',
|
||||
'focus-visible:inset-ring-2 focus-visible:inset-ring-state-accent-solid',
|
||||
'data-popup-open:bg-state-base-hover',
|
||||
'disabled:cursor-not-allowed disabled:text-text-disabled disabled:hover:bg-transparent disabled:hover:text-text-disabled',
|
||||
)}
|
||||
disabled={readonly}
|
||||
>
|
||||
<span className="i-ri-attachment-line size-5" aria-hidden="true" />
|
||||
</button>
|
||||
)
|
||||
},
|
||||
[readonly, t],
|
||||
)
|
||||
const renderTrigger = useCallback(() => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t(($) => $['fileUploader.uploadFromComputer'], { ns: 'common' })}
|
||||
className={cn(
|
||||
'inline-flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-lg p-1.5 text-text-tertiary outline-hidden',
|
||||
'hover:bg-state-base-hover hover:text-text-secondary',
|
||||
'focus-visible:inset-ring-2 focus-visible:inset-ring-state-accent-solid',
|
||||
'data-popup-open:bg-state-base-hover',
|
||||
'disabled:cursor-not-allowed disabled:text-text-disabled disabled:hover:bg-transparent disabled:hover:text-text-disabled',
|
||||
)}
|
||||
disabled={readonly}
|
||||
>
|
||||
<span className="i-ri-attachment-line size-5" aria-hidden="true" />
|
||||
</button>
|
||||
)
|
||||
}, [readonly, t])
|
||||
|
||||
return (
|
||||
<span className="inline-flex size-8 shrink-0 items-center justify-center">
|
||||
{readonly ? (
|
||||
renderTrigger(false)
|
||||
renderTrigger()
|
||||
) : (
|
||||
<FileFromLinkOrLocal
|
||||
trigger={renderTrigger}
|
||||
trigger={renderTrigger()}
|
||||
fileConfig={fileConfig}
|
||||
showFromLocal={fileConfig?.allowed_file_upload_methods?.includes(
|
||||
TransferMethod.local_file,
|
||||
|
||||
@ -155,7 +155,7 @@ describe('LanguageSelect', () => {
|
||||
|
||||
const trigger = screen.getByRole('combobox', { name: 'language' })
|
||||
expect(trigger).toBeDisabled()
|
||||
expect(trigger).toHaveClass('cursor-not-allowed')
|
||||
expect(trigger).toHaveAttribute('data-disabled')
|
||||
})
|
||||
|
||||
it('should not open the listbox when disabled', () => {
|
||||
|
||||
@ -36,8 +36,7 @@ const LanguageSelect: FC<ILanguageSelectProps> = ({ currentLanguage, onSelect, d
|
||||
aria-label="language"
|
||||
className={cn(
|
||||
'mx-1 w-auto shrink-0 bg-components-button-tertiary-bg text-components-button-tertiary-text hover:bg-components-button-tertiary-bg',
|
||||
disabled &&
|
||||
'cursor-not-allowed bg-components-button-tertiary-bg-disabled text-components-button-tertiary-text-disabled hover:bg-components-button-tertiary-bg-disabled',
|
||||
'data-disabled:cursor-not-allowed data-disabled:bg-components-button-tertiary-bg-disabled data-disabled:text-components-button-tertiary-text-disabled data-disabled:hover:bg-components-button-tertiary-bg-disabled',
|
||||
)}
|
||||
>
|
||||
<SelectValue placeholder={<span> </span>} />
|
||||
|
||||
@ -101,8 +101,11 @@ describe('CredentialSelector', () => {
|
||||
|
||||
// Act - Click trigger to open dropdown
|
||||
const trigger = screen.getByTestId('popover-trigger')
|
||||
expect(trigger).not.toHaveAttribute('data-popup-open')
|
||||
fireEvent.click(trigger)
|
||||
|
||||
expect(trigger).toHaveAttribute('data-popup-open', '')
|
||||
expect(trigger.firstElementChild).toHaveClass('bg-state-base-hover')
|
||||
// Assert - All credentials should be visible (current credential appears in both trigger and list)
|
||||
// Assert - All credentials should be visible (current credential appears in both trigger and list)
|
||||
expect(screen.getByTestId('popover-content'))!.toBeInTheDocument()
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import type { DataSourceCredential } from '@/types/pipeline'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import * as React from 'react'
|
||||
@ -37,9 +38,14 @@ const CredentialSelector = ({
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={set}>
|
||||
<PopoverTrigger nativeButton={false} render={<div className="grow overflow-hidden" />}>
|
||||
<Trigger currentCredential={currentCredential} isOpen={open} />
|
||||
</PopoverTrigger>
|
||||
<PopoverTrigger
|
||||
nativeButton={false}
|
||||
render={(props, state) => (
|
||||
<div {...props} className={cn('grow overflow-hidden', props.className)}>
|
||||
<Trigger currentCredential={currentCredential} isOpen={state.open} />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<PopoverContent
|
||||
placement="bottom-start"
|
||||
sideOffset={4}
|
||||
|
||||
@ -13,7 +13,14 @@ describe('ApiAccess', () => {
|
||||
const user = userEvent.setup()
|
||||
render(<ApiAccess expand apiEnabled />)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'common.appMenus.apiAccess' }))
|
||||
const trigger = screen.getByRole('button', { name: 'common.appMenus.apiAccess' })
|
||||
expect(trigger).not.toHaveAttribute('data-popup-open')
|
||||
expect(trigger.firstElementChild).toHaveClass('hover:bg-state-base-hover')
|
||||
|
||||
await user.click(trigger)
|
||||
|
||||
expect(trigger).toHaveAttribute('data-popup-open', '')
|
||||
expect(trigger.firstElementChild).toHaveClass('bg-state-base-hover')
|
||||
|
||||
expect(screen.getByText('API enabled')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
@ -20,13 +20,17 @@ const ApiAccess = ({ expand, apiEnabled }: ApiAccessProps) => {
|
||||
<div className={cn(expand ? 'px-1 py-2' : 'flex justify-center px-3 py-2')}>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger
|
||||
render={
|
||||
<button type="button" className="w-full border-none bg-transparent p-0 text-left">
|
||||
render={(props, state) => (
|
||||
<button
|
||||
{...props}
|
||||
type="button"
|
||||
className={cn('w-full border-none bg-transparent p-0 text-left', props.className)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex h-8 cursor-pointer items-center gap-2 rounded-lg border border-components-panel-border px-3',
|
||||
!expand && 'w-8 justify-center',
|
||||
open ? 'bg-state-base-hover' : 'hover:bg-state-base-hover',
|
||||
state.open ? 'bg-state-base-hover' : 'hover:bg-state-base-hover',
|
||||
)}
|
||||
>
|
||||
<ApiAggregate className="size-4 shrink-0 text-text-secondary" />
|
||||
@ -41,7 +45,7 @@ const ApiAccess = ({ expand, apiEnabled }: ApiAccessProps) => {
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
}
|
||||
)}
|
||||
/>
|
||||
<PopoverContent
|
||||
placement="top-start"
|
||||
|
||||
@ -30,7 +30,14 @@ describe('ServiceApi', () => {
|
||||
const user = userEvent.setup()
|
||||
render(<ServiceApi apiBaseUrl="https://api.example.com" />)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'dataset.serviceApi.title' }))
|
||||
const trigger = screen.getByRole('button', { name: 'dataset.serviceApi.title' })
|
||||
expect(trigger).not.toHaveAttribute('data-popup-open')
|
||||
expect(trigger.firstElementChild).toHaveClass('hover:bg-state-base-hover')
|
||||
|
||||
await user.click(trigger)
|
||||
|
||||
expect(trigger).toHaveAttribute('data-popup-open', '')
|
||||
expect(trigger.firstElementChild).toHaveClass('bg-state-base-hover')
|
||||
await user.click(screen.getByRole('button', { name: 'dataset.serviceApi.card.apiKey' }))
|
||||
|
||||
expect(screen.getByText('secret key modal')).toBeInTheDocument()
|
||||
|
||||
@ -33,12 +33,16 @@ const ServiceApi = ({ apiBaseUrl }: ServiceApiProps) => {
|
||||
<div className="flex items-center">
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger
|
||||
render={
|
||||
<button type="button" className="w-full border-none bg-transparent p-0 text-left">
|
||||
render={(props, state) => (
|
||||
<button
|
||||
{...props}
|
||||
type="button"
|
||||
className={cn('w-full border-none bg-transparent p-0 text-left', props.className)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex h-6 cursor-pointer items-center justify-center gap-1 overflow-hidden rounded-md px-1.5 py-1 text-text-tertiary',
|
||||
open ? 'bg-state-base-hover' : 'hover:bg-state-base-hover',
|
||||
state.open ? 'bg-state-base-hover' : 'hover:bg-state-base-hover',
|
||||
)}
|
||||
>
|
||||
<StatusDot className={cn('shrink-0')} status={apiBaseUrl ? 'success' : 'warning'} />
|
||||
@ -47,7 +51,7 @@ const ServiceApi = ({ apiBaseUrl }: ServiceApiProps) => {
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
}
|
||||
)}
|
||||
/>
|
||||
<PopoverContent
|
||||
placement="top-start"
|
||||
|
||||
@ -11,16 +11,18 @@ type TriggerArgs = {
|
||||
type DatePickerProps = {
|
||||
onChange: (value: Date | null) => void
|
||||
onClear: () => void
|
||||
renderTrigger: (args: TriggerArgs) => React.ReactNode
|
||||
renderTrigger: (
|
||||
props: React.HTMLAttributes<HTMLDivElement>,
|
||||
state: { open: boolean },
|
||||
args: TriggerArgs,
|
||||
) => React.ReactNode
|
||||
value?: Date
|
||||
}
|
||||
|
||||
// Mock the base date picker component
|
||||
vi.mock('@/app/components/base/date-and-time-picker/date-picker', () => ({
|
||||
default: ({ onChange, onClear, renderTrigger, value }: DatePickerProps) => {
|
||||
const trigger = renderTrigger({
|
||||
handleClickTrigger: () => {},
|
||||
})
|
||||
const trigger = renderTrigger({}, { open: false }, { handleClickTrigger: () => {} })
|
||||
return (
|
||||
<div role="group" aria-label="Date picker">
|
||||
{trigger}
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
import type { TriggerProps } from '@/app/components/base/date-and-time-picker/types'
|
||||
import type {
|
||||
DatePickerProps,
|
||||
TriggerProps,
|
||||
} from '@/app/components/base/date-and-time-picker/types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { RiCalendarLine, RiCloseCircleFill } from '@remixicon/react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
@ -31,8 +34,8 @@ const WrappedDatePicker = ({ className, label, value, onChange }: Props) => {
|
||||
[onChange],
|
||||
)
|
||||
|
||||
const renderTrigger = useCallback(
|
||||
({ handleClickTrigger }: TriggerProps) => {
|
||||
const renderTrigger = useCallback<NonNullable<DatePickerProps['renderTrigger']>>(
|
||||
(props, _state, { handleClickTrigger }: TriggerProps) => {
|
||||
const hasValue = Boolean(value)
|
||||
const triggerText = value
|
||||
? formatTimestamp(
|
||||
@ -44,9 +47,11 @@ const WrappedDatePicker = ({ className, label, value, onChange }: Props) => {
|
||||
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={cn(
|
||||
'group flex items-center rounded-md bg-components-input-bg-normal',
|
||||
className,
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
<button
|
||||
|
||||
@ -62,8 +62,8 @@ const renderAccountDropdown = () => {
|
||||
|
||||
return renderWithConsoleQuery(
|
||||
<AccountDropdown
|
||||
trigger={({ isOpen, ariaLabel }) => (
|
||||
<button type="button" aria-label={ariaLabel} data-open={isOpen}>
|
||||
trigger={({ ariaLabel }) => (
|
||||
<button type="button" aria-label={ariaLabel}>
|
||||
Current account
|
||||
</button>
|
||||
)}
|
||||
@ -120,12 +120,12 @@ describe('AccountDropdown', () => {
|
||||
renderAccountDropdown()
|
||||
|
||||
const trigger = screen.getByRole('button', { name: 'common.account.account' })
|
||||
expect(trigger).toHaveAttribute('data-open', 'false')
|
||||
expect(trigger).not.toHaveAttribute('data-popup-open')
|
||||
|
||||
await user.click(trigger)
|
||||
|
||||
expect(await screen.findByText('current@example.com')).toBeInTheDocument()
|
||||
expect(trigger).toHaveAttribute('data-open', 'true')
|
||||
expect(trigger).toHaveAttribute('data-popup-open', '')
|
||||
expect(screen.getByText('common.settings.preferences')).toBeInTheDocument()
|
||||
expect(screen.getByText('common.account.appearanceLabel')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
@ -14,7 +14,7 @@ import { useLogout } from '@/service/use-common'
|
||||
import { MainNavMenuContent } from './main-nav-menu-content'
|
||||
|
||||
type AccountDropdownProps = {
|
||||
trigger: (props: { isOpen: boolean; ariaLabel: string }) => ReactElement
|
||||
trigger: (props: { ariaLabel: string }) => ReactElement
|
||||
}
|
||||
|
||||
const mainNavMenuPopupClassName =
|
||||
@ -50,7 +50,6 @@ export default function AccountDropdown({ trigger }: AccountDropdownProps) {
|
||||
<DropdownMenuTrigger
|
||||
disabled={isHydrating}
|
||||
render={trigger({
|
||||
isOpen: isAccountMenuOpen,
|
||||
ariaLabel: t(($) => $['account.account'], { ns: 'common' }),
|
||||
})}
|
||||
/>
|
||||
|
||||
@ -72,10 +72,28 @@ describe('WorkspaceRoleCheckboxList', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('radio', { name: /First role/i })).toBeInTheDocument()
|
||||
const selectedRole = screen.getByRole('radio', { name: /First role/i })
|
||||
const unselectedRole = screen.getByRole('radio', { name: /Second role/i })
|
||||
expect(selectedRole).toHaveAttribute('data-checked', '')
|
||||
expect(unselectedRole).not.toHaveAttribute('data-checked')
|
||||
expect(screen.queryByRole('checkbox', { name: /First role/i })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should expose disabled state on single-role options', () => {
|
||||
render(
|
||||
<WorkspaceRoleCheckboxList
|
||||
selectedRoleIds={['role-1']}
|
||||
selectedRoles={[mockRoles[0]!]}
|
||||
allowMultipleRoles={false}
|
||||
disabledRoleIds={['role-1']}
|
||||
onSelectedRolesChange={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('radio', { name: /First role/i })).toHaveAttribute('data-disabled', '')
|
||||
expect(screen.getByRole('radio', { name: /Second role/i })).not.toHaveAttribute('data-disabled')
|
||||
})
|
||||
|
||||
it('should show legacy role descriptions when only one role is allowed', () => {
|
||||
vi.mocked(useWorkspaceRoleList).mockReturnValue({
|
||||
data: {
|
||||
|
||||
@ -202,7 +202,7 @@ describe('AccessRuleSection', () => {
|
||||
expect(onCreate).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should keep row actions when workspace role management is allowed', () => {
|
||||
it('should keep row actions when workspace role management is allowed', async () => {
|
||||
mocks.workspacePermissionKeys = ['workspace.role.manage']
|
||||
|
||||
renderWithQueryClient(
|
||||
@ -215,7 +215,12 @@ describe('AccessRuleSection', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('button', { name: 'common.operation.moreActions' })).toBeInTheDocument()
|
||||
const trigger = screen.getByRole('button', { name: 'common.operation.moreActions' })
|
||||
expect(trigger).not.toHaveAttribute('data-popup-open')
|
||||
|
||||
await userEvent.click(trigger)
|
||||
|
||||
expect(trigger).toHaveAttribute('data-popup-open', '')
|
||||
})
|
||||
|
||||
it('should hide create action when workspace role management is missing', () => {
|
||||
|
||||
@ -78,7 +78,7 @@ const AccessRuleRowMenu = ({ rule, onView, onEdit }: AccessRuleRowMenuProps) =>
|
||||
render={
|
||||
<ActionButton
|
||||
size="l"
|
||||
className={open ? 'bg-state-base-hover' : ''}
|
||||
className="data-popup-open:bg-state-base-hover"
|
||||
aria-label={t(($) => $['operation.moreActions'], { ns: 'common' })}
|
||||
/>
|
||||
}
|
||||
|
||||
@ -93,11 +93,18 @@ describe('ApiBasedExtensionSelector', () => {
|
||||
it('should open dropdown when clicked', async () => {
|
||||
// Act
|
||||
render(<ApiBasedExtensionSelector value="" onChange={mockOnChange} />)
|
||||
const popoverTrigger = screen.getByTestId('popover-trigger')
|
||||
const trigger = screen.getByText('common.apiBasedExtension.selector.placeholder')
|
||||
const arrow = trigger.parentElement?.querySelector('[aria-hidden="true"]')
|
||||
expect(popoverTrigger).not.toHaveAttribute('data-popup-open')
|
||||
expect(arrow).toHaveClass('opacity-60')
|
||||
|
||||
fireEvent.click(trigger)
|
||||
|
||||
// Assert
|
||||
// Assert
|
||||
expect(popoverTrigger).toHaveAttribute('data-popup-open', '')
|
||||
expect(arrow).not.toHaveClass('opacity-60')
|
||||
expect(
|
||||
await screen.findByText('common.apiBasedExtension.selector.title'),
|
||||
)!.toBeInTheDocument()
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useQueryState } from 'nuqs'
|
||||
@ -41,8 +42,12 @@ export function ApiBasedExtensionSelector({ value, onChange }: ApiBasedExtension
|
||||
<>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger
|
||||
render={
|
||||
<button type="button" className="block w-full border-0 bg-transparent p-0 text-left">
|
||||
render={(props, state) => (
|
||||
<button
|
||||
{...props}
|
||||
type="button"
|
||||
className={cn('block w-full border-0 bg-transparent p-0 text-left', props.className)}
|
||||
>
|
||||
{currentItem ? (
|
||||
<div className="flex h-9 cursor-pointer items-center justify-between rounded-lg bg-components-input-bg-normal pr-2.5 pl-3">
|
||||
<div className="text-sm text-text-primary">{currentItem.name}</div>
|
||||
@ -51,7 +56,10 @@ export function ApiBasedExtensionSelector({ value, onChange }: ApiBasedExtension
|
||||
{currentItem.api_endpoint}
|
||||
</div>
|
||||
<span
|
||||
className={`i-ri-arrow-down-s-line size-4 text-text-secondary ${!open && 'opacity-60'}`}
|
||||
className={cn(
|
||||
'i-ri-arrow-down-s-line size-4 text-text-secondary',
|
||||
!state.open && 'opacity-60',
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
@ -62,13 +70,16 @@ export function ApiBasedExtensionSelector({ value, onChange }: ApiBasedExtension
|
||||
ns: 'common',
|
||||
})}
|
||||
<span
|
||||
className={`i-ri-arrow-down-s-line h-4 w-4 text-text-secondary ${!open && 'opacity-60'}`}
|
||||
className={cn(
|
||||
'i-ri-arrow-down-s-line h-4 w-4 text-text-secondary',
|
||||
!state.open && 'opacity-60',
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
}
|
||||
)}
|
||||
/>
|
||||
<PopoverContent
|
||||
placement="bottom-start"
|
||||
|
||||
@ -101,7 +101,7 @@ const DataSourcePluginActions = ({ detail, onUpdate }: Props) => {
|
||||
pluginID={detail.plugin_id}
|
||||
currentVersion={detail.version}
|
||||
onSelect={handleVersionSelect}
|
||||
trigger={
|
||||
trigger={() => (
|
||||
<Badge
|
||||
className="h-5 px-1.5"
|
||||
text={
|
||||
@ -118,7 +118,7 @@ const DataSourcePluginActions = ({ detail, onUpdate }: Props) => {
|
||||
hasRedCornerMark={hasNewVersion}
|
||||
uppercase={false}
|
||||
/>
|
||||
}
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{canUpdatePlugin && (hasNewVersion || isFromGitHub) && (
|
||||
|
||||
@ -302,21 +302,27 @@ vi.mock('@/app/components/base/date-and-time-picker/time-picker', () => ({
|
||||
}: {
|
||||
value?: string | { format: (format: string) => string }
|
||||
onChange: (value: { hour: () => number; minute: () => number }) => void
|
||||
renderTrigger: (params: {
|
||||
inputElem: ReactNode
|
||||
onClick: () => void
|
||||
isOpen: boolean
|
||||
}) => ReactNode
|
||||
renderTrigger: (
|
||||
props: Record<string, never>,
|
||||
state: { open: boolean },
|
||||
params: {
|
||||
inputElem: ReactNode
|
||||
onClick: () => void
|
||||
},
|
||||
) => ReactNode
|
||||
}) => {
|
||||
const displayValue = typeof value === 'string' ? value : value?.format('HH:mm')
|
||||
|
||||
return (
|
||||
<div data-testid="update-time-picker">
|
||||
{renderTrigger({
|
||||
inputElem: <span data-testid="update-time-value">{displayValue}</span>,
|
||||
onClick: vi.fn(),
|
||||
isOpen: false,
|
||||
})}
|
||||
{renderTrigger(
|
||||
{},
|
||||
{ open: false },
|
||||
{
|
||||
inputElem: <span data-testid="update-time-value">{displayValue}</span>,
|
||||
onClick: vi.fn(),
|
||||
},
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
|
||||
@ -106,9 +106,13 @@ describe('AddCustomModel', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('popover-trigger'))
|
||||
const trigger = screen.getByTestId('popover-trigger')
|
||||
expect(trigger).not.toHaveAttribute('data-popup-open')
|
||||
|
||||
fireEvent.click(trigger)
|
||||
|
||||
// The portal should be "open"
|
||||
expect(trigger).toHaveAttribute('data-popup-open', '')
|
||||
expect(screen.getByTestId('popover')).toHaveAttribute('data-open', 'true')
|
||||
expect(screen.getByText('gpt-4')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('model-icon')).toBeInTheDocument()
|
||||
|
||||
@ -98,7 +98,11 @@ const AddCustomModel = ({
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger
|
||||
nativeButton={false}
|
||||
render={<div className="inline-block">{renderTrigger(open)}</div>}
|
||||
render={(props, state) => (
|
||||
<div {...props} className={cn('inline-block', props.className)}>
|
||||
{renderTrigger(state.open)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<PopoverContent
|
||||
placement="bottom-end"
|
||||
|
||||
@ -119,7 +119,13 @@ describe('Authorized', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
const trigger = screen.getByTestId('popover-trigger')
|
||||
expect(trigger).not.toHaveAttribute('data-popup-open')
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /trigger\s*closed/i }))
|
||||
|
||||
expect(trigger).toHaveAttribute('data-popup-open', '')
|
||||
expect(screen.getByRole('button', { name: /trigger\s*open/i })).toBeInTheDocument()
|
||||
expect(screen.getByTestId('authorized-item'))!.toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: /addApiKey/i }))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
@ -177,11 +177,14 @@ const Authorized = ({
|
||||
<Popover open={mergedIsOpen} onOpenChange={setMergedIsOpen}>
|
||||
<PopoverTrigger
|
||||
nativeButton={false}
|
||||
render={
|
||||
<div className={triggerPopupSameWidth ? 'w-full' : 'inline-block'}>
|
||||
{renderTrigger(mergedIsOpen)}
|
||||
render={(props, state) => (
|
||||
<div
|
||||
{...props}
|
||||
className={cn(triggerPopupSameWidth ? 'w-full' : 'inline-block', props.className)}
|
||||
>
|
||||
{renderTrigger(state.open)}
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
onClick={handleTriggerClick}
|
||||
/>
|
||||
<PopoverContent
|
||||
|
||||
@ -367,9 +367,13 @@ describe('ModelParameterModal', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
const trigger = screen.getByText('Custom Closed').closest('button')
|
||||
expect(trigger).not.toHaveAttribute('data-popup-open')
|
||||
|
||||
fireEvent.click(screen.getByText('Custom Closed'))
|
||||
|
||||
expect(screen.getByText('Custom Open')).toBeInTheDocument()
|
||||
expect(trigger).toHaveAttribute('data-popup-open', '')
|
||||
expect(screen.getByTestId('model-selector')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByText('hide'))
|
||||
|
||||
@ -126,20 +126,24 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
|
||||
>
|
||||
{renderTrigger ? (
|
||||
<PopoverTrigger
|
||||
render={
|
||||
render={(props, state) => (
|
||||
<button
|
||||
{...props}
|
||||
type="button"
|
||||
className="block w-full border-none bg-transparent p-0 text-left text-inherit [font:inherit]"
|
||||
className={cn(
|
||||
'block w-full border-none bg-transparent p-0 text-left text-inherit [font:inherit]',
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{renderTrigger({
|
||||
open,
|
||||
open: state.open,
|
||||
currentProvider,
|
||||
currentModel,
|
||||
providerName: provider,
|
||||
modelId,
|
||||
})}
|
||||
</button>
|
||||
}
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-8 min-w-74 items-center gap-px overflow-hidden rounded-lg">
|
||||
|
||||
@ -83,12 +83,12 @@ vi.mock('@/app/components/plugins/update-plugin/plugin-version-picker', () => ({
|
||||
onSelect,
|
||||
disabled,
|
||||
}: {
|
||||
trigger: ReactNode
|
||||
trigger: (open: boolean) => ReactNode
|
||||
onSelect: (state: { version: string; unique_identifier: string; isDowngrade?: boolean }) => void
|
||||
disabled?: boolean
|
||||
}) => (
|
||||
<div data-testid="plugin-version-picker" data-disabled={String(Boolean(disabled))}>
|
||||
{trigger}
|
||||
{trigger(false)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
|
||||
@ -93,7 +93,7 @@ const ProviderCardActions: FC<Props> = ({ detail, onUpdate }) => {
|
||||
onSelect={handleVersionSelect}
|
||||
sideOffset={4}
|
||||
alignOffset={0}
|
||||
trigger={
|
||||
trigger={() => (
|
||||
<Badge
|
||||
className={cn(
|
||||
canUpdatePlugin &&
|
||||
@ -111,7 +111,7 @@ const ProviderCardActions: FC<Props> = ({ detail, onUpdate }) => {
|
||||
}
|
||||
hasRedCornerMark={hasNewVersion}
|
||||
/>
|
||||
}
|
||||
)}
|
||||
/>
|
||||
{isDebuggingPlugin && (
|
||||
<Badge
|
||||
|
||||
@ -73,7 +73,12 @@ const createRole = (overrides: Partial<Role> = {}): Role => ({
|
||||
|
||||
const openMenu = async () => {
|
||||
const user = userEvent.setup()
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.moreActions' }))
|
||||
const trigger = screen.getByRole('button', { name: 'common.operation.moreActions' })
|
||||
expect(trigger).not.toHaveAttribute('data-popup-open')
|
||||
|
||||
await user.click(trigger)
|
||||
|
||||
expect(trigger).toHaveAttribute('data-popup-open', '')
|
||||
const menus = screen.getAllByRole('menu')
|
||||
return {
|
||||
user,
|
||||
|
||||
@ -9,7 +9,6 @@ import {
|
||||
AlertDialogDescription,
|
||||
AlertDialogTitle,
|
||||
} from '@langgenius/dify-ui/alert-dialog'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@ -114,7 +113,7 @@ const RowMenu = ({ roleCategory, role, onView, onEdit }: RowMenuProps) => {
|
||||
render={
|
||||
<ActionButton
|
||||
size="m"
|
||||
className={cn('shrink-0', open && 'bg-state-base-hover')}
|
||||
className="shrink-0 data-popup-open:bg-state-base-hover"
|
||||
aria-label={t(($) => $['operation.moreActions'], { ns: 'common' })}
|
||||
/>
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { ReactElement, ReactNode } from 'react'
|
||||
import type { TriggerParams } from '@/app/components/base/date-and-time-picker/types'
|
||||
import type { ReactNode } from 'react'
|
||||
import type { TimePickerProps } from '@/app/components/base/date-and-time-picker/types'
|
||||
import type { AutoUpdateConfig } from '@/app/components/plugins/reference-setting-modal/auto-update-setting/types'
|
||||
import type { dayjsToTimeOfDay } from '@/app/components/plugins/reference-setting-modal/auto-update-setting/utils'
|
||||
import type { PluginCategoryEnum } from '@/app/components/plugins/types'
|
||||
@ -39,7 +39,7 @@ type UpdateSettingDialogFormProps = {
|
||||
onPluginsChange: (newPlugins: string[]) => void
|
||||
onRequestClose: () => void
|
||||
onUpdateTimeChange: (value: Parameters<typeof dayjsToTimeOfDay>[0]) => void
|
||||
renderTimePickerTrigger: (params: TriggerParams) => ReactElement
|
||||
renderTimePickerTrigger: NonNullable<TimePickerProps['renderTrigger']>
|
||||
}
|
||||
|
||||
const updateSettingFormLabelClassName =
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
'use client'
|
||||
|
||||
import type { TriggerParams } from '@/app/components/base/date-and-time-picker/types'
|
||||
import type {
|
||||
TimePickerProps,
|
||||
TriggerParams,
|
||||
} from '@/app/components/base/date-and-time-picker/types'
|
||||
import type { AutoUpdateConfig } from '@/app/components/plugins/reference-setting-modal/auto-update-setting/types'
|
||||
import type { PluginCategoryEnum } from '@/app/components/plugins/types'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
@ -203,19 +206,26 @@ const UpdateSettingDialog = ({ category, disabled = false }: Props) => {
|
||||
setDraftAutoUpgrade(undefined)
|
||||
setIsOpen(false)
|
||||
}, [autoUpgrade, saveAutoUpgrade])
|
||||
const renderTimePickerTrigger = useCallback(
|
||||
({ inputElem, onClick, isOpen }: TriggerParams) => {
|
||||
const renderTimePickerTrigger = useCallback<NonNullable<TimePickerProps['renderTrigger']>>(
|
||||
(props, state, { inputElem, onClick }: TriggerParams) => {
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
type="button"
|
||||
className="group flex h-8 w-full cursor-pointer items-center gap-1 rounded-lg border-none bg-components-input-bg-normal px-2 py-1 text-left shadow-none hover:bg-state-base-hover-alt focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
'group flex h-8 w-full cursor-pointer items-center gap-1 rounded-lg border-none bg-components-input-bg-normal px-2 py-1 text-left shadow-none hover:bg-state-base-hover-alt focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden',
|
||||
props.className,
|
||||
)}
|
||||
onClick={(event) => {
|
||||
onClick(event)
|
||||
props.onClick?.(event)
|
||||
}}
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn(
|
||||
'i-ri-time-line size-4 shrink-0 text-text-tertiary',
|
||||
isOpen ? 'text-text-secondary' : 'group-hover:text-text-secondary',
|
||||
state.open ? 'text-text-secondary' : 'group-hover:text-text-secondary',
|
||||
)}
|
||||
/>
|
||||
<span className="min-w-0 flex-1 p-1 system-sm-regular text-components-input-text-filled">
|
||||
|
||||
@ -293,7 +293,6 @@ const WorkspaceRoleCheckboxList = ({
|
||||
render={<ul />}
|
||||
>
|
||||
{filteredRoles.map((role) => {
|
||||
const checked = selectedRoleIdSet.has(role.id)
|
||||
const disabled = disabledRoleIdSet.has(role.id)
|
||||
|
||||
return (
|
||||
@ -307,8 +306,8 @@ const WorkspaceRoleCheckboxList = ({
|
||||
type="button"
|
||||
className={cn(
|
||||
'flex w-full cursor-pointer items-start gap-3 rounded-lg border-0 bg-transparent px-3 py-2.5 text-left hover:bg-state-base-hover focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-components-input-border-active',
|
||||
checked && 'bg-state-accent-hover hover:bg-state-accent-hover',
|
||||
disabled && 'cursor-not-allowed opacity-50 hover:bg-transparent',
|
||||
'data-checked:bg-state-accent-hover data-checked:hover:bg-state-accent-hover',
|
||||
'data-disabled:cursor-not-allowed data-disabled:opacity-50 data-disabled:hover:bg-transparent data-checked:data-disabled:hover:bg-transparent',
|
||||
)}
|
||||
/>
|
||||
}
|
||||
|
||||
@ -977,7 +977,12 @@ describe('MainNav', () => {
|
||||
|
||||
renderMainNav({ enable_learn_app: true })
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.mainNav.help.openMenu' }))
|
||||
const helpTrigger = screen.getByRole('button', { name: 'common.mainNav.help.openMenu' })
|
||||
expect(helpTrigger).not.toHaveAttribute('data-popup-open')
|
||||
|
||||
fireEvent.click(helpTrigger)
|
||||
|
||||
expect(helpTrigger).toHaveAttribute('data-popup-open', '')
|
||||
const learnDifyItem = await screen.findByRole('menuitemcheckbox', {
|
||||
name: 'common.mainNav.help.learnDify',
|
||||
})
|
||||
|
||||
@ -332,7 +332,14 @@ describe('WorkspaceCard', () => {
|
||||
it('opens workspace actions and switcher in a popover panel', async () => {
|
||||
renderWorkspaceCard()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.mainNav.workspace.openMenu' }))
|
||||
const workspaceTrigger = screen.getByRole('button', {
|
||||
name: 'common.mainNav.workspace.openMenu',
|
||||
})
|
||||
expect(workspaceTrigger).not.toHaveAttribute('data-popup-open')
|
||||
|
||||
fireEvent.click(workspaceTrigger)
|
||||
|
||||
expect(workspaceTrigger).toHaveAttribute('data-popup-open', '')
|
||||
|
||||
const panel = await screen.findByRole('dialog', { name: 'Solar Studio' })
|
||||
expect(panel).toBeInTheDocument()
|
||||
@ -420,7 +427,14 @@ describe('WorkspaceCard', () => {
|
||||
|
||||
expect(defaultWorkspaceOptions).toEqual(['Atlas Workspace', 'Solar Studio', 'Evan Workspace'])
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.mainNav.workspace.sort.openMenu' }))
|
||||
const sortTrigger = screen.getByRole('button', {
|
||||
name: 'common.mainNav.workspace.sort.openMenu',
|
||||
})
|
||||
expect(sortTrigger).not.toHaveAttribute('data-popup-open')
|
||||
|
||||
fireEvent.click(sortTrigger)
|
||||
|
||||
expect(sortTrigger).toHaveAttribute('data-popup-open', '')
|
||||
|
||||
expect(
|
||||
await screen.findByRole('menuitemradio', {
|
||||
|
||||
@ -18,7 +18,7 @@ const AccountSection = ({ compact = false }: AccountSectionProps) => {
|
||||
|
||||
return (
|
||||
<AccountDropdown
|
||||
trigger={({ isOpen, ariaLabel }) => (
|
||||
trigger={({ ariaLabel }) => (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={ariaLabel}
|
||||
@ -26,7 +26,7 @@ const AccountSection = ({ compact = false }: AccountSectionProps) => {
|
||||
className={cn(
|
||||
'flex min-w-0 shrink items-center rounded-full text-left text-components-main-nav-text transition-colors hover:bg-state-base-hover focus-visible:inset-ring-2 focus-visible:inset-ring-state-accent-solid focus-visible:outline-hidden disabled:cursor-default disabled:hover:bg-transparent',
|
||||
compact ? 'justify-center p-1' : 'max-w-45 gap-3 py-1 pr-4 pl-1',
|
||||
isOpen && 'bg-state-base-hover',
|
||||
'data-popup-open:bg-state-base-hover',
|
||||
)}
|
||||
>
|
||||
<Avatar
|
||||
|
||||
@ -144,7 +144,7 @@ const HelpMenu = ({ triggerIcon = defaultTriggerIcon, triggerClassName }: HelpMe
|
||||
className={cn(
|
||||
'inline-flex size-7 shrink-0 cursor-pointer items-center justify-center rounded-full border border-components-card-border bg-components-card-bg p-0 text-text-tertiary shadow-xs transition-colors hover:bg-components-card-bg-alt hover:text-saas-dify-blue-inverted focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden',
|
||||
triggerClassName,
|
||||
open && 'bg-components-card-bg-alt text-saas-dify-blue-inverted',
|
||||
'data-popup-open:bg-components-card-bg-alt data-popup-open:text-saas-dify-blue-inverted',
|
||||
skipRecoveryVisible && styles.stepByStepTourRecoveryPulse,
|
||||
)}
|
||||
>
|
||||
|
||||
@ -85,7 +85,6 @@ function WorkspaceCreditsLabel({ credits, unit }: { credits: string; unit: strin
|
||||
}
|
||||
|
||||
function WorkspaceCardTrigger({
|
||||
open,
|
||||
name,
|
||||
status,
|
||||
credits,
|
||||
@ -96,7 +95,6 @@ function WorkspaceCardTrigger({
|
||||
onPrefetchWorkspaces,
|
||||
onPlanClick,
|
||||
}: {
|
||||
open: boolean
|
||||
name: string
|
||||
status: ReactNode
|
||||
credits: number
|
||||
@ -122,7 +120,7 @@ function WorkspaceCardTrigger({
|
||||
className={cn(
|
||||
'flex w-full items-center gap-1.5 py-1.5 pr-3 pl-1.5 text-left transition-colors hover:bg-state-base-hover focus-visible:inset-ring-2 focus-visible:inset-ring-state-accent-solid focus-visible:outline-hidden',
|
||||
showCloudBilling ? 'rounded-t-xl' : 'rounded-xl',
|
||||
open && 'bg-linear-to-b from-background-section-burn to-background-section',
|
||||
'data-popup-open:bg-linear-to-b data-popup-open:from-background-section-burn data-popup-open:to-background-section',
|
||||
)}
|
||||
>
|
||||
<WorkspaceAvatar name={name} size="sm" />
|
||||
@ -309,7 +307,6 @@ export function WorkspaceCard() {
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<>
|
||||
<WorkspaceCardTrigger
|
||||
open={open}
|
||||
name={currentWorkspace.name}
|
||||
status={renderWorkspaceStatus()}
|
||||
credits={currentWorkspace.credits}
|
||||
|
||||
@ -77,7 +77,7 @@ function WorkspaceSwitchControls({
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
workspaceSwitchActionButtonClassName,
|
||||
sortMenuOpen && 'bg-state-base-hover text-text-secondary',
|
||||
'data-popup-open:bg-state-base-hover data-popup-open:text-text-secondary',
|
||||
)}
|
||||
>
|
||||
<span aria-hidden className={workspaceSwitchActionIconWrapClassName}>
|
||||
|
||||
@ -30,7 +30,6 @@ function TagsFilter({ tags, onTagsChange, usedInMarketplace = false }: TagsFilte
|
||||
{usedInMarketplace && (
|
||||
<MarketplaceTrigger
|
||||
selectedTagsLength={selectedTagsLength}
|
||||
open={open}
|
||||
tags={tags}
|
||||
tagsMap={tagsMap}
|
||||
onTagsChange={onTagsChange}
|
||||
@ -39,7 +38,6 @@ function TagsFilter({ tags, onTagsChange, usedInMarketplace = false }: TagsFilte
|
||||
{!usedInMarketplace && (
|
||||
<ToolSelectorTrigger
|
||||
selectedTagsLength={selectedTagsLength}
|
||||
open={open}
|
||||
tags={tags}
|
||||
tagsMap={tagsMap}
|
||||
onTagsChange={onTagsChange}
|
||||
|
||||
@ -18,7 +18,6 @@ describe('MarketplaceTrigger', () => {
|
||||
<Popover>
|
||||
<MarketplaceTrigger
|
||||
selectedTagsLength={0}
|
||||
open={false}
|
||||
tags={[]}
|
||||
tagsMap={tagsMap}
|
||||
onTagsChange={vi.fn()}
|
||||
@ -37,7 +36,6 @@ describe('MarketplaceTrigger', () => {
|
||||
<Popover>
|
||||
<MarketplaceTrigger
|
||||
selectedTagsLength={3}
|
||||
open
|
||||
tags={['agent', 'rag', 'search']}
|
||||
tagsMap={tagsMap}
|
||||
onTagsChange={vi.fn()}
|
||||
@ -58,7 +56,6 @@ describe('MarketplaceTrigger', () => {
|
||||
<Popover>
|
||||
<MarketplaceTrigger
|
||||
selectedTagsLength={tags.length}
|
||||
open={false}
|
||||
tags={tags}
|
||||
tagsMap={tagsMap}
|
||||
onTagsChange={setTags}
|
||||
@ -80,4 +77,26 @@ describe('MarketplaceTrigger', () => {
|
||||
).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'pluginTags.allTags' })).toHaveFocus()
|
||||
})
|
||||
|
||||
it('reflects the actual popover state on the trigger', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<Popover>
|
||||
<MarketplaceTrigger
|
||||
selectedTagsLength={0}
|
||||
tags={[]}
|
||||
tagsMap={tagsMap}
|
||||
onTagsChange={vi.fn()}
|
||||
/>
|
||||
</Popover>,
|
||||
)
|
||||
|
||||
const trigger = screen.getByRole('button', { name: 'pluginTags.allTags' })
|
||||
expect(trigger).not.toHaveAttribute('data-popup-open')
|
||||
|
||||
await user.click(trigger)
|
||||
|
||||
expect(trigger).toHaveAttribute('data-popup-open', '')
|
||||
})
|
||||
})
|
||||
|
||||
@ -18,7 +18,6 @@ describe('ToolSelectorTrigger', () => {
|
||||
<Popover>
|
||||
<ToolSelectorTrigger
|
||||
selectedTagsLength={0}
|
||||
open={false}
|
||||
tags={[]}
|
||||
tagsMap={tagsMap}
|
||||
onTagsChange={vi.fn()}
|
||||
@ -38,7 +37,6 @@ describe('ToolSelectorTrigger', () => {
|
||||
<Popover>
|
||||
<ToolSelectorTrigger
|
||||
selectedTagsLength={3}
|
||||
open
|
||||
tags={['agent', 'rag', 'search']}
|
||||
tagsMap={tagsMap}
|
||||
onTagsChange={vi.fn()}
|
||||
@ -61,7 +59,6 @@ describe('ToolSelectorTrigger', () => {
|
||||
<Popover>
|
||||
<ToolSelectorTrigger
|
||||
selectedTagsLength={0}
|
||||
open={false}
|
||||
tags={[]}
|
||||
tagsMap={tagsMap}
|
||||
onTagsChange={vi.fn()}
|
||||
@ -71,12 +68,14 @@ describe('ToolSelectorTrigger', () => {
|
||||
)
|
||||
|
||||
const trigger = screen.getByRole('button', { name: 'pluginTags.allTags' })
|
||||
expect(trigger).not.toHaveAttribute('data-popup-open')
|
||||
await user.tab()
|
||||
expect(trigger).toHaveFocus()
|
||||
|
||||
await user.keyboard('{Enter}')
|
||||
|
||||
expect(screen.getByText('Tag options')).toBeInTheDocument()
|
||||
expect(trigger).toHaveAttribute('data-popup-open', '')
|
||||
})
|
||||
|
||||
it('keeps clear as a separate action from the popover trigger', async () => {
|
||||
@ -87,7 +86,6 @@ describe('ToolSelectorTrigger', () => {
|
||||
<Popover>
|
||||
<ToolSelectorTrigger
|
||||
selectedTagsLength={tags.length}
|
||||
open={false}
|
||||
tags={tags}
|
||||
tagsMap={tagsMap}
|
||||
onTagsChange={setTags}
|
||||
|
||||
@ -7,7 +7,6 @@ import { useTranslation } from '#i18n'
|
||||
|
||||
type MarketplaceTriggerProps = {
|
||||
selectedTagsLength: number
|
||||
open: boolean
|
||||
tags: string[]
|
||||
tagsMap: Record<string, Tag>
|
||||
onTagsChange: (tags: string[]) => void
|
||||
@ -15,7 +14,6 @@ type MarketplaceTriggerProps = {
|
||||
|
||||
function MarketplaceTrigger({
|
||||
selectedTagsLength,
|
||||
open,
|
||||
tags,
|
||||
tagsMap,
|
||||
onTagsChange,
|
||||
@ -48,7 +46,7 @@ function MarketplaceTrigger({
|
||||
'h-8 justify-start px-2 py-1 text-text-tertiary focus-visible:ring-inset',
|
||||
!!selectedTagsLength &&
|
||||
'border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg pr-8 shadow-xs shadow-shadow-shadow-3',
|
||||
open && !selectedTagsLength && 'bg-state-base-hover',
|
||||
!selectedTagsLength && 'data-popup-open:bg-state-base-hover',
|
||||
)}
|
||||
>
|
||||
<span className="p-0.5">
|
||||
|
||||
@ -7,7 +7,6 @@ import { useTranslation } from '#i18n'
|
||||
|
||||
type ToolSelectorTriggerProps = {
|
||||
selectedTagsLength: number
|
||||
open: boolean
|
||||
tags: string[]
|
||||
tagsMap: Record<string, Tag>
|
||||
onTagsChange: (tags: string[]) => void
|
||||
@ -15,7 +14,6 @@ type ToolSelectorTriggerProps = {
|
||||
|
||||
function ToolSelectorTrigger({
|
||||
selectedTagsLength,
|
||||
open,
|
||||
tags,
|
||||
tagsMap,
|
||||
onTagsChange,
|
||||
@ -49,7 +47,7 @@ function ToolSelectorTrigger({
|
||||
!selectedTagsLength && 'size-7 min-h-0 justify-center p-0',
|
||||
!!selectedTagsLength &&
|
||||
'border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg py-0.5 pr-7 pl-1 shadow-xs shadow-shadow-shadow-3',
|
||||
open && !selectedTagsLength && 'bg-state-base-hover',
|
||||
!selectedTagsLength && 'data-popup-open:bg-state-base-hover',
|
||||
)}
|
||||
>
|
||||
<span className={cn('shrink-0', !!selectedTagsLength && 'p-0.5')}>
|
||||
|
||||
@ -188,7 +188,7 @@ describe('Authorized Component', () => {
|
||||
expect(screen.getByRole('button'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render with custom trigger when renderTrigger is provided', () => {
|
||||
it('should render a custom trigger from the actual popover state', () => {
|
||||
const pluginPayload = createPluginPayload()
|
||||
const credentials = [createCredential()]
|
||||
|
||||
@ -203,8 +203,14 @@ describe('Authorized Component', () => {
|
||||
{ wrapper: createWrapper() },
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('custom-trigger'))!.toBeInTheDocument()
|
||||
const trigger = screen.getByTestId('popover-trigger')
|
||||
expect(trigger).not.toHaveAttribute('data-popup-open')
|
||||
expect(screen.getByText('Closed'))!.toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByTestId('custom-trigger'))
|
||||
|
||||
expect(trigger).toHaveAttribute('data-popup-open', '')
|
||||
expect(screen.getByText('Open')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show singular authorization text for 1 credential', () => {
|
||||
|
||||
@ -207,16 +207,16 @@ const Authorized = ({
|
||||
<>
|
||||
<Popover open={mergedIsOpen} onOpenChange={setMergedIsOpen}>
|
||||
<PopoverTrigger
|
||||
render={
|
||||
<div className={triggerPopupSameWidth ? 'w-full' : 'inline-block'}>
|
||||
render={(props, state) => (
|
||||
<div
|
||||
{...props}
|
||||
className={cn(triggerPopupSameWidth ? 'w-full' : 'inline-block', props.className)}
|
||||
>
|
||||
{renderTrigger ? (
|
||||
renderTrigger(mergedIsOpen)
|
||||
renderTrigger(state.open)
|
||||
) : (
|
||||
<Button
|
||||
className={cn(
|
||||
'w-full',
|
||||
mergedIsOpen && 'bg-components-button-secondary-bg-hover',
|
||||
)}
|
||||
className={cn('w-full', state.open && 'bg-components-button-secondary-bg-hover')}
|
||||
>
|
||||
<StatusDot
|
||||
className="mr-2"
|
||||
@ -233,7 +233,7 @@ const Authorized = ({
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
/>
|
||||
<PopoverContent
|
||||
placement={placement}
|
||||
|
||||
@ -120,7 +120,12 @@ describe('AppSelector', () => {
|
||||
|
||||
renderWithQueryClient(<AppSelector onSelect={onSelect} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'app.appSelector.label' }))
|
||||
const trigger = screen.getByRole('button', { name: 'app.appSelector.label' })
|
||||
expect(trigger).not.toHaveAttribute('data-popup-open')
|
||||
|
||||
fireEvent.click(trigger)
|
||||
|
||||
expect(trigger).toHaveAttribute('data-popup-open', '')
|
||||
expect(screen.getByText('app.appSelector.label')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByRole('combobox', { name: 'app.appSelector.label' }))
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import type { Placement } from '@langgenius/dify-ui/popover'
|
||||
import type { App } from '@/types/app'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import { keepPreviousData, useInfiniteQuery } from '@tanstack/react-query'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
@ -126,12 +127,16 @@ export function AppSelector({
|
||||
<PopoverTrigger
|
||||
aria-label={t(($) => $['appSelector.label'], { ns: 'app' })}
|
||||
disabled={disabled}
|
||||
render={
|
||||
<button type="button" className="block w-full border-0 bg-transparent p-0 text-left" />
|
||||
}
|
||||
>
|
||||
<AppTrigger open={isShow} appDetail={currentAppInfo} />
|
||||
</PopoverTrigger>
|
||||
render={(props, state) => (
|
||||
<button
|
||||
{...props}
|
||||
type="button"
|
||||
className={cn('block w-full border-0 bg-transparent p-0 text-left', props.className)}
|
||||
>
|
||||
<AppTrigger open={state.open} appDetail={currentAppInfo} />
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
<PopoverContent
|
||||
placement={placement}
|
||||
sideOffset={offset}
|
||||
|
||||
@ -209,12 +209,12 @@ const DetailHeader = ({
|
||||
pluginID={plugin_id}
|
||||
currentVersion={version}
|
||||
onSelect={handleVersionSelect}
|
||||
trigger={
|
||||
trigger={(open) => (
|
||||
<Badge
|
||||
className={cn(
|
||||
'mx-1',
|
||||
versionPicker.isShow && 'bg-state-base-hover',
|
||||
(versionPicker.isShow || (canUpdatePlugin && isFromMarketplace)) &&
|
||||
open && 'bg-state-base-hover',
|
||||
(open || (canUpdatePlugin && isFromMarketplace)) &&
|
||||
'hover:bg-state-base-hover',
|
||||
)}
|
||||
uppercase={false}
|
||||
@ -231,7 +231,7 @@ const DetailHeader = ({
|
||||
}
|
||||
hasRedCornerMark={hasNewVersion}
|
||||
/>
|
||||
}
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@ -343,7 +343,7 @@ describe('ModelParameterModal', () => {
|
||||
expect(screen.queryByTestId('trigger')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call renderTrigger with correct props', () => {
|
||||
it('should call renderTrigger with the actual popover state', async () => {
|
||||
// Arrange
|
||||
const renderTrigger = vi.fn().mockReturnValue(<div>Custom</div>)
|
||||
const value = { provider: 'openai', model: 'gpt-4' }
|
||||
@ -352,7 +352,8 @@ describe('ModelParameterModal', () => {
|
||||
// Act
|
||||
render(<ModelParameterModal {...props} />)
|
||||
|
||||
// Assert
|
||||
const trigger = screen.getByText('Custom').closest('button')
|
||||
expect(trigger).not.toHaveAttribute('data-popup-open')
|
||||
expect(renderTrigger).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
open: false,
|
||||
@ -360,6 +361,19 @@ describe('ModelParameterModal', () => {
|
||||
modelId: 'gpt-4',
|
||||
}),
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByText('Custom'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(renderTrigger).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
open: true,
|
||||
providerName: 'openai',
|
||||
modelId: 'gpt-4',
|
||||
}),
|
||||
)
|
||||
})
|
||||
expect(trigger).toHaveAttribute('data-popup-open', '')
|
||||
})
|
||||
|
||||
it('should not render portal content when closed', () => {
|
||||
|
||||
@ -224,14 +224,18 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
|
||||
</div>
|
||||
) : (
|
||||
<PopoverTrigger
|
||||
render={
|
||||
render={(props, state) => (
|
||||
<button
|
||||
{...props}
|
||||
type="button"
|
||||
className="block w-full border-none bg-transparent p-0 text-left text-inherit [font:inherit]"
|
||||
className={cn(
|
||||
'block w-full border-none bg-transparent p-0 text-left text-inherit [font:inherit]',
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{renderTrigger ? (
|
||||
renderTrigger({
|
||||
open,
|
||||
open: state.open,
|
||||
currentProvider,
|
||||
currentModel,
|
||||
providerName: value?.provider,
|
||||
@ -249,7 +253,7 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
}
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<PopoverContent
|
||||
|
||||
@ -31,9 +31,28 @@ vi.mock('@langgenius/dify-ui/popover', async () => {
|
||||
return <PopoverContext.Provider value={{ open, setOpen }}>{children}</PopoverContext.Provider>
|
||||
}
|
||||
|
||||
const PopoverTrigger = ({ render }: { render: React.ReactNode }) => {
|
||||
type TriggerProps = React.HTMLAttributes<HTMLElement> & {
|
||||
'data-popup-open'?: string
|
||||
'data-testid'?: string
|
||||
}
|
||||
|
||||
const PopoverTrigger = ({
|
||||
render,
|
||||
}: {
|
||||
render:
|
||||
| React.ReactNode
|
||||
| ((props: TriggerProps, state: { open: boolean }) => React.ReactElement)
|
||||
}) => {
|
||||
const { open, setOpen } = React.useContext(PopoverContext)
|
||||
return <div onClick={() => setOpen(!open)}>{render}</div>
|
||||
const props: TriggerProps = {
|
||||
'data-testid': 'popover-trigger',
|
||||
'data-popup-open': open ? '' : undefined,
|
||||
onClick: () => setOpen(!open),
|
||||
}
|
||||
|
||||
if (typeof render === 'function') return render(props, { open })
|
||||
|
||||
return <div {...props}>{render}</div>
|
||||
}
|
||||
|
||||
const PopoverContent = ({ children }: { children: React.ReactNode }) => {
|
||||
@ -112,8 +131,12 @@ describe('SubscriptionSelectorEntry', () => {
|
||||
it('should render placeholder when open without selection', () => {
|
||||
render(<SubscriptionSelectorEntry selectedId={undefined} onSelect={vi.fn()} />)
|
||||
|
||||
const trigger = screen.getByTestId('popover-trigger')
|
||||
expect(trigger).not.toHaveAttribute('data-popup-open')
|
||||
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
expect(trigger).toHaveAttribute('data-popup-open', '')
|
||||
expect(screen.getByText('pluginTrigger.subscription.selectPlaceholder')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
|
||||
@ -106,11 +106,11 @@ export const SubscriptionSelectorEntry = ({
|
||||
return (
|
||||
<Popover open={isOpen} onOpenChange={setIsOpen}>
|
||||
<PopoverTrigger
|
||||
render={
|
||||
<div>
|
||||
<SubscriptionTriggerButton selectedId={selectedId} isOpen={isOpen} />
|
||||
render={(props, state) => (
|
||||
<div {...props}>
|
||||
<SubscriptionTriggerButton selectedId={selectedId} isOpen={state.open} />
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
/>
|
||||
<PopoverContent
|
||||
placement="bottom-start"
|
||||
|
||||
@ -109,9 +109,17 @@ vi.mock('@langgenius/dify-ui/dropdown-menu', async () => {
|
||||
onClick,
|
||||
render,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
children?: React.ReactNode
|
||||
onClick?: React.MouseEventHandler<HTMLElement>
|
||||
render?: React.ReactElement
|
||||
render?:
|
||||
| React.ReactElement
|
||||
| ((
|
||||
props: React.HTMLAttributes<HTMLElement> & {
|
||||
'data-testid'?: string
|
||||
'data-popup-open'?: string
|
||||
},
|
||||
state: { open: boolean },
|
||||
) => React.ReactElement)
|
||||
}) => {
|
||||
const { isOpen, setOpen } = useDropdownMenuContext()
|
||||
const handleClick = (e: React.MouseEvent<HTMLElement>) => {
|
||||
@ -119,6 +127,16 @@ vi.mock('@langgenius/dify-ui/dropdown-menu', async () => {
|
||||
setOpen(!isOpen)
|
||||
}
|
||||
|
||||
if (typeof render === 'function')
|
||||
return render(
|
||||
{
|
||||
'data-testid': 'dropdown-trigger',
|
||||
'data-popup-open': isOpen ? '' : undefined,
|
||||
onClick: handleClick,
|
||||
},
|
||||
{ open: isOpen },
|
||||
)
|
||||
|
||||
if (render)
|
||||
return React.cloneElement(
|
||||
render,
|
||||
@ -243,10 +261,12 @@ describe('InstallPluginDropdown', () => {
|
||||
expect(screen.getByTestId('arrow-down-icon')).toHaveClass('ml-1', 'size-4')
|
||||
expect(trigger).toHaveClass('custom-trigger')
|
||||
expect(trigger).toHaveAttribute('data-variant', 'primary')
|
||||
expect(trigger).not.toHaveAttribute('data-popup-open')
|
||||
|
||||
fireEvent.click(trigger)
|
||||
|
||||
expect(trigger).toHaveClass('custom-open')
|
||||
expect(trigger).toHaveAttribute('data-popup-open', '')
|
||||
expect(screen.getByTestId('dropdown-content')).toHaveClass('custom-popup')
|
||||
})
|
||||
|
||||
|
||||
@ -160,8 +160,9 @@ const InstallPluginDropdown = ({
|
||||
accept={SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS}
|
||||
/>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
render={(props, state) => (
|
||||
<Button
|
||||
{...props}
|
||||
variant={triggerVariant}
|
||||
disabled={disabled}
|
||||
title={buttonLabel}
|
||||
@ -169,19 +170,18 @@ const InstallPluginDropdown = ({
|
||||
className={cn(
|
||||
'size-full p-2',
|
||||
triggerClassName,
|
||||
!disabled && isMenuOpen && triggerOpenClassName,
|
||||
state.open && triggerOpenClassName,
|
||||
props.className,
|
||||
)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<>
|
||||
<RiAddCircleFill className="size-4 shrink-0" />
|
||||
<span className={cn(showTriggerArrow ? 'pl-1' : 'min-w-0 flex-1 px-0.5 text-left')}>
|
||||
{buttonLabel}
|
||||
</span>
|
||||
{showTriggerArrow && <RiArrowDownSLine className="ml-1 size-4" />}
|
||||
</>
|
||||
</DropdownMenuTrigger>
|
||||
>
|
||||
<RiAddCircleFill className="size-4 shrink-0" />
|
||||
<span className={cn(showTriggerArrow ? 'pl-1' : 'min-w-0 flex-1 px-0.5 text-left')}>
|
||||
{buttonLabel}
|
||||
</span>
|
||||
{showTriggerArrow && <RiArrowDownSLine className="ml-1 size-4" />}
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
<DropdownMenuContent
|
||||
placement="bottom-start"
|
||||
sideOffset={4}
|
||||
|
||||
@ -125,23 +125,28 @@ const PluginTasks = ({
|
||||
<DropdownMenu open={open} onOpenChange={setOpen}>
|
||||
<DropdownMenuTrigger
|
||||
nativeButton={false}
|
||||
render={<div className={canOpenMenu ? 'cursor-pointer' : 'cursor-default'} />}
|
||||
render={(props, state) => (
|
||||
<div
|
||||
{...props}
|
||||
className={cn('cursor-pointer data-disabled:cursor-default', props.className)}
|
||||
>
|
||||
<TaskStatusIndicator
|
||||
tip={tip}
|
||||
isInstalling={isInstalling}
|
||||
isInstallingWithSuccess={isInstallingWithSuccess}
|
||||
isInstallingWithError={isInstallingWithError}
|
||||
isSuccess={isSuccess}
|
||||
isFailed={isFailed}
|
||||
isOpen={state.open}
|
||||
successPluginsLength={successPluginsLength}
|
||||
runningPluginsLength={runningPluginsLength}
|
||||
totalPluginsLength={totalPluginsLength}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
disabled={!canOpenMenu}
|
||||
>
|
||||
<TaskStatusIndicator
|
||||
tip={tip}
|
||||
isInstalling={isInstalling}
|
||||
isInstallingWithSuccess={isInstallingWithSuccess}
|
||||
isInstallingWithError={isInstallingWithError}
|
||||
isSuccess={isSuccess}
|
||||
isFailed={isFailed}
|
||||
isOpen={open}
|
||||
successPluginsLength={successPluginsLength}
|
||||
runningPluginsLength={runningPluginsLength}
|
||||
totalPluginsLength={totalPluginsLength}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
/>
|
||||
<DropdownMenuContent
|
||||
placement={dropdownPlacement}
|
||||
sideOffset={4}
|
||||
|
||||
@ -172,21 +172,17 @@ vi.mock('@/app/components/base/date-and-time-picker/time-picker', () => ({
|
||||
onChange: (v: unknown) => void
|
||||
onClear: () => void
|
||||
title?: string
|
||||
renderTrigger: (params: {
|
||||
inputElem: React.ReactNode
|
||||
onClick: () => void
|
||||
isOpen: boolean
|
||||
}) => React.ReactNode
|
||||
renderTrigger: (
|
||||
props: React.HTMLAttributes<HTMLElement>,
|
||||
state: { open: boolean },
|
||||
params: { inputElem: React.ReactNode; onClick: () => void },
|
||||
) => React.ReactNode
|
||||
}) => {
|
||||
const inputElem = <span data-testid="time-input">{value.format('HH:mm')}</span>
|
||||
|
||||
return (
|
||||
<div data-testid="time-picker">
|
||||
{renderTrigger({
|
||||
inputElem,
|
||||
onClick: () => {},
|
||||
isOpen: false,
|
||||
})}
|
||||
{renderTrigger({}, { open: false }, { inputElem, onClick: () => {} })}
|
||||
<div data-testid="time-picker-dropdown">
|
||||
<button
|
||||
data-testid="time-picker-set"
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { AutoUpdateConfig } from './types'
|
||||
import type { TriggerParams } from '@/app/components/base/date-and-time-picker/types'
|
||||
import type {
|
||||
TimePickerProps,
|
||||
TriggerParams,
|
||||
} from '@/app/components/base/date-and-time-picker/types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { SegmentedControl, SegmentedControlItem } from '@langgenius/dify-ui/segmented-control'
|
||||
import { RiTimeLine } from '@remixicon/react'
|
||||
@ -138,19 +141,26 @@ const AutoUpdateSetting: FC<Props> = ({ payload, onChange }) => {
|
||||
[payload, onChange],
|
||||
)
|
||||
|
||||
const renderTimePickerTrigger = useCallback(
|
||||
({ inputElem, onClick, isOpen }: TriggerParams) => {
|
||||
const renderTimePickerTrigger = useCallback<NonNullable<TimePickerProps['renderTrigger']>>(
|
||||
(props, state, { inputElem, onClick }: TriggerParams) => {
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
type="button"
|
||||
className="group flex h-8 w-40 cursor-pointer items-center justify-between rounded-lg border-none bg-components-input-bg-normal px-2 text-left hover:bg-state-base-hover-alt focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
'group flex h-8 w-40 cursor-pointer items-center justify-between rounded-lg border-none bg-components-input-bg-normal px-2 text-left hover:bg-state-base-hover-alt focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden',
|
||||
props.className,
|
||||
)}
|
||||
onClick={(event) => {
|
||||
onClick(event)
|
||||
props.onClick?.(event)
|
||||
}}
|
||||
>
|
||||
<div className="flex w-0 grow items-center gap-x-1">
|
||||
<RiTimeLine
|
||||
className={cn(
|
||||
'size-4 shrink-0 text-text-tertiary',
|
||||
isOpen ? 'text-text-secondary' : 'group-hover:text-text-secondary',
|
||||
state.open ? 'text-text-secondary' : 'group-hover:text-text-secondary',
|
||||
)}
|
||||
/>
|
||||
{inputElem}
|
||||
|
||||
@ -905,7 +905,7 @@ describe('update-plugin', () => {
|
||||
onShowChange: vi.fn(),
|
||||
pluginID: 'test-plugin-id',
|
||||
currentVersion: '1.0.0',
|
||||
trigger: <span>Select Version</span>,
|
||||
trigger: () => <span>Select Version</span>,
|
||||
onSelect: vi.fn(),
|
||||
}
|
||||
|
||||
@ -1136,7 +1136,7 @@ describe('update-plugin', () => {
|
||||
onShowChange: vi.fn(),
|
||||
pluginID: 'test',
|
||||
currentVersion: '1.0.0',
|
||||
trigger: <span>Select</span>,
|
||||
trigger: () => <span>Select</span>,
|
||||
onSelect: vi.fn(),
|
||||
}}
|
||||
/>,
|
||||
|
||||
@ -50,7 +50,7 @@ describe('PluginVersionPicker', () => {
|
||||
onShowChange={vi.fn()}
|
||||
pluginID="plugin-1"
|
||||
currentVersion="2.0.0"
|
||||
trigger={<span>trigger</span>}
|
||||
trigger={() => <span>trigger</span>}
|
||||
onSelect={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
@ -61,6 +61,23 @@ describe('PluginVersionPicker', () => {
|
||||
expect(screen.getByText('CURRENT')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('passes Base trigger state and props through the preserved button root', () => {
|
||||
render(
|
||||
<PluginVersionPicker
|
||||
isShow
|
||||
onShowChange={vi.fn()}
|
||||
pluginID="plugin-1"
|
||||
currentVersion="2.0.0"
|
||||
trigger={(open) => <span>{open ? 'open trigger' : 'closed trigger'}</span>}
|
||||
onSelect={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
const trigger = screen.getByRole('button', { name: 'open trigger' })
|
||||
expect(trigger).toHaveAttribute('data-popup-open', '')
|
||||
expect(trigger).toHaveClass('cursor-pointer')
|
||||
})
|
||||
|
||||
it('renders figma-aligned version rows', () => {
|
||||
render(
|
||||
<PluginVersionPicker
|
||||
@ -68,7 +85,7 @@ describe('PluginVersionPicker', () => {
|
||||
onShowChange={vi.fn()}
|
||||
pluginID="plugin-1"
|
||||
currentVersion="2.0.0"
|
||||
trigger={<span>trigger</span>}
|
||||
trigger={() => <span>trigger</span>}
|
||||
onSelect={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
@ -98,7 +115,7 @@ describe('PluginVersionPicker', () => {
|
||||
onShowChange={onShowChange}
|
||||
pluginID="plugin-1"
|
||||
currentVersion="2.0.0"
|
||||
trigger={<span>trigger</span>}
|
||||
trigger={() => <span>trigger</span>}
|
||||
onSelect={onSelect}
|
||||
/>,
|
||||
)
|
||||
@ -122,7 +139,7 @@ describe('PluginVersionPicker', () => {
|
||||
onShowChange={vi.fn()}
|
||||
pluginID="plugin-1"
|
||||
currentVersion="2.0.0"
|
||||
trigger={<span>trigger</span>}
|
||||
trigger={() => <span>trigger</span>}
|
||||
onSelect={onSelect}
|
||||
/>,
|
||||
)
|
||||
|
||||
@ -17,7 +17,7 @@ type Props = Readonly<{
|
||||
onShowChange: (isShow: boolean) => void
|
||||
pluginID: string
|
||||
currentVersion: string
|
||||
trigger: React.ReactNode
|
||||
trigger: (open: boolean) => React.ReactNode
|
||||
placement?: Placement
|
||||
sideOffset?: number
|
||||
alignOffset?: number
|
||||
@ -76,10 +76,19 @@ const PluginVersionPicker: FC<Props> = ({
|
||||
>
|
||||
<PopoverTrigger
|
||||
disabled={disabled}
|
||||
className={cn('inline-flex cursor-pointer items-center', disabled && 'cursor-default')}
|
||||
>
|
||||
{trigger}
|
||||
</PopoverTrigger>
|
||||
render={(props, state) => (
|
||||
<button
|
||||
{...props}
|
||||
type="button"
|
||||
className={cn(
|
||||
'inline-flex cursor-pointer items-center data-disabled:cursor-default',
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{trigger(state.open)}
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
|
||||
<PopoverContent
|
||||
placement={placement}
|
||||
|
||||
@ -52,7 +52,7 @@ const StartNodeSelectionPanel: FC<StartNodeSelectionPanelProps> = ({
|
||||
BlockEnum.TriggerWebhook,
|
||||
BlockEnum.TriggerPlugin,
|
||||
]}
|
||||
trigger={() => (
|
||||
trigger={
|
||||
<StartNodeOption
|
||||
icon={
|
||||
<div className="flex h-9 w-9 items-center justify-center rounded-[10px] border-[0.5px] border-transparent bg-util-colors-blue-brand-blue-brand-500 p-2">
|
||||
@ -63,7 +63,7 @@ const StartNodeSelectionPanel: FC<StartNodeSelectionPanelProps> = ({
|
||||
description={t(($) => $['onboarding.triggerDescription'], { ns: 'workflow' })}
|
||||
onClick={() => setShowTriggerSelector(true)}
|
||||
/>
|
||||
)}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -107,8 +107,10 @@ describe('BlockSelector', () => {
|
||||
onSelect={onSelect}
|
||||
blocks={[createBlock(BlockEnum.LLM, 'LLM'), createBlock(BlockEnum.End, 'End')]}
|
||||
availableBlocksTypes={[BlockEnum.LLM, BlockEnum.End]}
|
||||
trigger={(open) => (
|
||||
<button type="button">{open ? 'selector-open' : 'selector-closed'}</button>
|
||||
trigger={(props, state) => (
|
||||
<button {...props} type="button">
|
||||
{state.open ? 'selector-open' : 'selector-closed'}
|
||||
</button>
|
||||
)}
|
||||
/>,
|
||||
)
|
||||
@ -155,8 +157,10 @@ describe('BlockSelector', () => {
|
||||
blocks={[createBlock(BlockEnum.LLM, 'LLM')]}
|
||||
availableBlocksTypes={[BlockEnum.LLM, BlockEnum.Start]}
|
||||
showStartTab
|
||||
trigger={(open) => (
|
||||
<button type="button">{open ? 'selector-open' : 'selector-closed'}</button>
|
||||
trigger={(props, state) => (
|
||||
<button {...props} type="button">
|
||||
{state.open ? 'selector-open' : 'selector-closed'}
|
||||
</button>
|
||||
)}
|
||||
/>,
|
||||
)
|
||||
@ -192,7 +196,7 @@ describe('BlockSelector', () => {
|
||||
blocks={[createBlock(BlockEnum.LLM, 'LLM')]}
|
||||
availableBlocksTypes={[BlockEnum.LLM, BlockEnum.Start]}
|
||||
showStartTab
|
||||
trigger={() => <button type="button">selector-open</button>}
|
||||
trigger={<button type="button">selector-open</button>}
|
||||
/>,
|
||||
)
|
||||
|
||||
@ -218,8 +222,10 @@ describe('BlockSelector', () => {
|
||||
blocks={[createBlock(BlockEnum.LLM, 'LLM')]}
|
||||
availableBlocksTypes={[BlockEnum.LLM, BlockEnum.Start]}
|
||||
showStartTab
|
||||
trigger={(open) => (
|
||||
<button type="button">{open ? 'selector-open' : 'selector-closed'}</button>
|
||||
trigger={(props, state) => (
|
||||
<button {...props} type="button">
|
||||
{state.open ? 'selector-open' : 'selector-closed'}
|
||||
</button>
|
||||
)}
|
||||
/>,
|
||||
)
|
||||
@ -255,8 +261,10 @@ describe('BlockSelector', () => {
|
||||
onSelect={vi.fn()}
|
||||
blocks={[createBlock(BlockEnum.LLM, 'LLM')]}
|
||||
availableBlocksTypes={[BlockEnum.LLM]}
|
||||
trigger={(open) => (
|
||||
<button type="button">{open ? 'selector-open' : 'selector-closed'}</button>
|
||||
trigger={(props, state) => (
|
||||
<button {...props} type="button">
|
||||
{state.open ? 'selector-open' : 'selector-closed'}
|
||||
</button>
|
||||
)}
|
||||
/>,
|
||||
)
|
||||
@ -287,7 +295,7 @@ describe('BlockSelector', () => {
|
||||
onOpenChange={setOpen}
|
||||
onSelect={vi.fn()}
|
||||
blocks={[createBlock(BlockEnum.LLM, 'LLM')]}
|
||||
trigger={() => <button type="button">selector-trigger</button>}
|
||||
trigger={<button type="button">selector-trigger</button>}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -314,7 +322,7 @@ describe('BlockSelector', () => {
|
||||
<BlockSelector
|
||||
onSelect={vi.fn()}
|
||||
blocks={[createBlock(BlockEnum.LLM, 'LLM')]}
|
||||
trigger={() => <button type="button">selector-trigger</button>}
|
||||
trigger={<button type="button">selector-trigger</button>}
|
||||
/>,
|
||||
)
|
||||
|
||||
@ -339,11 +347,11 @@ describe('BlockSelector', () => {
|
||||
onSelect={vi.fn()}
|
||||
blocks={[createBlock(BlockEnum.LLM, 'LLM')]}
|
||||
availableBlocksTypes={[BlockEnum.LLM]}
|
||||
trigger={() => (
|
||||
trigger={
|
||||
<button type="button" onClick={onTriggerClick}>
|
||||
open-selector
|
||||
</button>
|
||||
)}
|
||||
}
|
||||
/>,
|
||||
)
|
||||
|
||||
@ -369,7 +377,7 @@ describe('BlockSelector', () => {
|
||||
onSelect={vi.fn()}
|
||||
blocks={[createBlock(BlockEnum.LLM, 'LLM')]}
|
||||
availableBlocksTypes={[BlockEnum.LLM]}
|
||||
trigger={() => <ForwardingButtonTrigger />}
|
||||
trigger={<ForwardingButtonTrigger />}
|
||||
/>,
|
||||
)
|
||||
|
||||
@ -388,7 +396,7 @@ describe('BlockSelector', () => {
|
||||
onSelect={vi.fn()}
|
||||
blocks={[createBlock(BlockEnum.LLM, 'LLM')]}
|
||||
availableBlocksTypes={[BlockEnum.LLM]}
|
||||
trigger={() => <Button variant="primary">open-shared-button-trigger</Button>}
|
||||
trigger={<Button variant="primary">open-shared-button-trigger</Button>}
|
||||
/>,
|
||||
)
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { Placement } from '@langgenius/dify-ui/popover'
|
||||
import type { CSSProperties, KeyboardEvent, MouseEventHandler, ReactElement } from 'react'
|
||||
import type { Placement, PopoverTriggerProps } from '@langgenius/dify-ui/popover'
|
||||
import type { CSSProperties, KeyboardEvent, MouseEventHandler } from 'react'
|
||||
import type {
|
||||
CommonNodeType,
|
||||
NodeDefault,
|
||||
@ -32,13 +32,13 @@ export type BlockSelectorProps = {
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
onSelect: OnSelectBlock
|
||||
trigger?: (open: boolean) => ReactElement
|
||||
trigger?: NonNullable<PopoverTriggerProps['render']>
|
||||
triggerTooltip?: string
|
||||
placement?: Placement
|
||||
sideOffset?: number
|
||||
alignOffset?: number
|
||||
triggerStyle?: CSSProperties
|
||||
triggerClassName?: (open: boolean) => string
|
||||
triggerClassName?: string
|
||||
triggerAriaLabel?: string
|
||||
popupClassName?: string
|
||||
availableBlocksTypes?: BlockEnum[]
|
||||
@ -125,7 +125,7 @@ function BlockSelector({
|
||||
<PopoverTrigger
|
||||
aria-label={triggerAriaLabel}
|
||||
disabled={disabled}
|
||||
render={trigger(open)}
|
||||
render={trigger}
|
||||
onClick={handleTrigger}
|
||||
/>
|
||||
) : (
|
||||
@ -136,7 +136,7 @@ function BlockSelector({
|
||||
<Button
|
||||
variant="primary"
|
||||
size="small"
|
||||
className={cn('z-10 size-4 rounded-full p-0', triggerClassName?.(open))}
|
||||
className={cn('z-10 size-4 rounded-full p-0', triggerClassName)}
|
||||
style={triggerStyle}
|
||||
/>
|
||||
}
|
||||
|
||||
@ -68,10 +68,11 @@ const SnippetTagsFilter = ({ embedded = false, value, onChange }: SnippetTagsFil
|
||||
value.length > 0 &&
|
||||
'border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg py-0.5 pr-1.5 pl-1 shadow-xs shadow-shadow-shadow-3',
|
||||
!embedded && 'hover:bg-components-input-bg-hover',
|
||||
open &&
|
||||
(embedded
|
||||
? !value.length && 'bg-state-base-hover'
|
||||
: 'border-components-input-border-active bg-components-input-bg-active text-text-secondary'),
|
||||
embedded &&
|
||||
!value.length &&
|
||||
'data-popup-open:bg-state-base-hover data-popup-open:hover:bg-components-button-ghost-bg-hover',
|
||||
!embedded &&
|
||||
'data-popup-open:border-components-input-border-active data-popup-open:bg-components-input-bg-active data-popup-open:text-text-secondary data-popup-open:hover:bg-components-input-bg-hover',
|
||||
value.length > 0 && 'text-text-secondary',
|
||||
)}
|
||||
>
|
||||
|
||||
@ -147,7 +147,7 @@ const CustomEdge = ({
|
||||
nextNodeTargetHandle: targetHandleId || 'target',
|
||||
}}
|
||||
availableBlocksTypes={intersection(availablePrevBlocks, availableNextBlocks)}
|
||||
triggerClassName={() => 'transition-transform hover:scale-150'}
|
||||
triggerClassName="transition-transform hover:scale-150"
|
||||
/>
|
||||
</div>
|
||||
</EdgeLabelRenderer>
|
||||
|
||||
@ -59,12 +59,13 @@ const WorkflowChecklist = ({ disabled, showGoTo = true, onItemClick }: WorkflowC
|
||||
return (
|
||||
<Popover open={open} onOpenChange={(newOpen) => !disabled && setOpen(newOpen)}>
|
||||
<PopoverTrigger
|
||||
disabled={disabled}
|
||||
render={
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
'group relative ml-0.5 flex size-7 items-center justify-center rounded-md border-none bg-transparent p-0',
|
||||
disabled && 'cursor-not-allowed opacity-50',
|
||||
'data-disabled:cursor-not-allowed data-disabled:opacity-50',
|
||||
)}
|
||||
disabled={disabled || undefined}
|
||||
aria-label={checklistLabel}
|
||||
|
||||
@ -38,8 +38,7 @@ function UndoRedo({ handleUndo, handleRedo }: UndoRedoProps) {
|
||||
focusableWhenDisabled
|
||||
className={cn(
|
||||
'size-8 p-0 text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary',
|
||||
(nodesReadOnly || buttonsDisabled.undo) &&
|
||||
'cursor-not-allowed text-text-disabled hover:bg-transparent hover:text-text-disabled',
|
||||
'data-disabled:cursor-not-allowed data-disabled:text-text-disabled data-disabled:hover:bg-transparent data-disabled:hover:text-text-disabled',
|
||||
)}
|
||||
onClick={handleUndo}
|
||||
>
|
||||
@ -56,8 +55,7 @@ function UndoRedo({ handleUndo, handleRedo }: UndoRedoProps) {
|
||||
focusableWhenDisabled
|
||||
className={cn(
|
||||
'size-8 p-0 text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary',
|
||||
(nodesReadOnly || buttonsDisabled.redo) &&
|
||||
'cursor-not-allowed text-text-disabled hover:bg-transparent hover:text-text-disabled',
|
||||
'data-disabled:cursor-not-allowed data-disabled:text-text-disabled data-disabled:hover:bg-transparent data-disabled:hover:text-text-disabled',
|
||||
)}
|
||||
onClick={handleRedo}
|
||||
>
|
||||
|
||||
@ -164,8 +164,7 @@ const ViewWorkflowHistory = () => {
|
||||
aria-label={t(($) => $['changeHistory.title'], { ns: 'workflow' })}
|
||||
className={cn(
|
||||
'size-8 p-0 text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary',
|
||||
nodesReadOnly &&
|
||||
'cursor-not-allowed text-text-disabled hover:bg-transparent hover:text-text-disabled',
|
||||
'data-disabled:cursor-not-allowed data-disabled:text-text-disabled data-disabled:hover:bg-transparent data-disabled:hover:text-text-disabled',
|
||||
)}
|
||||
onClick={() => {
|
||||
if (nodesReadOnly) return
|
||||
|
||||
@ -31,7 +31,7 @@ vi.mock('@/app/components/workflow/block-selector', () => ({
|
||||
isolateKeyboardEvents,
|
||||
}: any) => (
|
||||
<div>
|
||||
<div>{trigger()}</div>
|
||||
<div>{typeof trigger === 'function' ? trigger({}, { open: false }) : trigger}</div>
|
||||
<div>{`available:${(availableBlocksTypes || []).join(',')}`}</div>
|
||||
<div>{`show-start:${String(showStartTab)}`}</div>
|
||||
<div>{`ignore:${(ignoreNodeIds || []).join(',')}`}</div>
|
||||
|
||||
@ -60,21 +60,19 @@ export function ChangeBlockMenuTrigger({
|
||||
[handleNodeChange, nodeId, sourceHandle],
|
||||
)
|
||||
|
||||
const renderTrigger = useCallback(() => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="mx-1 flex h-8 w-[calc(100%-8px)] cursor-pointer items-center rounded-lg border-0 bg-transparent px-2 text-left text-sm text-text-secondary outline-hidden select-none hover:bg-state-base-hover focus-visible:bg-state-base-hover focus-visible:outline-hidden"
|
||||
>
|
||||
{t(($) => $['panel.changeBlock'], { ns: 'workflow' })}
|
||||
</button>
|
||||
)
|
||||
}, [t])
|
||||
const triggerElement = (
|
||||
<button
|
||||
type="button"
|
||||
className="mx-1 flex h-8 w-[calc(100%-8px)] cursor-pointer items-center rounded-lg border-0 bg-transparent px-2 text-left text-sm text-text-secondary outline-hidden select-none hover:bg-state-base-hover focus-visible:bg-state-base-hover focus-visible:outline-hidden"
|
||||
>
|
||||
{t(($) => $['panel.changeBlock'], { ns: 'workflow' })}
|
||||
</button>
|
||||
)
|
||||
|
||||
return (
|
||||
<BlockSelector
|
||||
onSelect={handleSelect}
|
||||
trigger={renderTrigger}
|
||||
trigger={triggerElement}
|
||||
popupClassName="min-w-[240px]"
|
||||
availableBlocksTypes={availableNodes}
|
||||
showStartTab={showStartTab}
|
||||
|
||||
@ -58,7 +58,7 @@ type BlockSelectorProps = {
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
onSelect?: (type: BlockEnum, pluginDefaultValue?: { pluginId: string }) => void
|
||||
triggerClassName?: (open: boolean) => string
|
||||
triggerClassName?: string
|
||||
}
|
||||
|
||||
vi.mock('reactflow', () => ({
|
||||
@ -83,7 +83,8 @@ vi.mock('@/app/components/workflow/block-selector', () => ({
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
className={triggerClassName?.(open)}
|
||||
className={triggerClassName}
|
||||
data-popup-open={open ? '' : undefined}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onOpenChange?.(!open)
|
||||
@ -214,14 +215,14 @@ describe('node-handle', () => {
|
||||
|
||||
fireEvent.click(addNodeButton)
|
||||
|
||||
expect(addNodeButton).toHaveClass('opacity-100')
|
||||
expect(addNodeButton).toHaveAttribute('data-popup-open')
|
||||
// Trigger stays pointer-events-none so it never steals mousedown from
|
||||
// the underlying React Flow handle (drag-to-connect must keep working).
|
||||
expect(addNodeButton).toHaveClass('pointer-events-none')
|
||||
|
||||
fireEvent.click(handle)
|
||||
|
||||
expect(addNodeButton).toHaveClass('opacity-0')
|
||||
expect(addNodeButton).not.toHaveAttribute('data-popup-open')
|
||||
|
||||
fireEvent.click(getSelectNodeButton())
|
||||
|
||||
@ -275,7 +276,7 @@ describe('node-handle', () => {
|
||||
|
||||
fireEvent.click(addNodeButton)
|
||||
|
||||
expect(addNodeButton).toHaveClass('opacity-100')
|
||||
expect(addNodeButton).toHaveAttribute('data-popup-open')
|
||||
expect(addNodeButton).toHaveClass('pointer-events-none')
|
||||
|
||||
fireEvent.click(getSelectNodeButton())
|
||||
@ -293,7 +294,7 @@ describe('node-handle', () => {
|
||||
|
||||
fireEvent.click(handle)
|
||||
|
||||
expect(addNodeButton).toHaveClass('opacity-0')
|
||||
expect(addNodeButton).not.toHaveAttribute('data-popup-open')
|
||||
})
|
||||
|
||||
it('should keep the source add trigger visible when the node is selected', () => {
|
||||
@ -323,7 +324,7 @@ describe('node-handle', () => {
|
||||
|
||||
const addNodeButton = getAddNodeButton()
|
||||
|
||||
expect(addNodeButton).toHaveClass('opacity-100')
|
||||
expect(addNodeButton).toHaveAttribute('data-popup-open')
|
||||
expect(addNodeButton).toHaveClass('pointer-events-none')
|
||||
expect(mockSetShouldAutoOpenStartNodeSelector).toHaveBeenCalledWith(false)
|
||||
expect(mockSetHasSelectedStartNode).toHaveBeenCalledWith(false)
|
||||
|
||||
@ -12,12 +12,8 @@ import { useNodesReadOnly } from '../../../../../hooks/use-workflow'
|
||||
import NextStep from '../index'
|
||||
|
||||
vi.mock('@/app/components/workflow/block-selector', () => ({
|
||||
default: ({ trigger }: { trigger: ((open: boolean) => ReactNode) | ReactNode }) => {
|
||||
return (
|
||||
<div data-testid="next-step-block-selector">
|
||||
{typeof trigger === 'function' ? trigger(false) : trigger}
|
||||
</div>
|
||||
)
|
||||
default: ({ trigger }: { trigger: ReactNode }) => {
|
||||
return <div data-testid="next-step-block-selector">{trigger}</div>
|
||||
},
|
||||
}))
|
||||
|
||||
|
||||
@ -81,15 +81,9 @@ vi.mock('@langgenius/dify-ui/button', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/block-selector', () => ({
|
||||
default: ({
|
||||
trigger,
|
||||
onSelect,
|
||||
}: {
|
||||
trigger: ((open: boolean) => ReactNode) | ReactNode
|
||||
onSelect: (type: BlockEnum) => void
|
||||
}) => (
|
||||
default: ({ trigger, onSelect }: { trigger: ReactNode; onSelect: (type: BlockEnum) => void }) => (
|
||||
<div>
|
||||
{typeof trigger === 'function' ? trigger(false) : trigger}
|
||||
{trigger}
|
||||
<button type="button" onClick={() => onSelect(BlockEnum.HttpRequest)}>
|
||||
select-http
|
||||
</button>
|
||||
|
||||
@ -53,22 +53,17 @@ const Add = ({ nodeId, nodeData, sourceHandle, isParallel, isFailBranch }: AddPr
|
||||
|
||||
return t(($) => $['panel.selectNextStep'], { ns: 'workflow' })
|
||||
}, [isFailBranch, isParallel, t])
|
||||
const renderTrigger = useCallback(
|
||||
(open: boolean) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="large"
|
||||
className={`bg-dropzone-bg hover:bg-dropzone-bg-hover relative w-full justify-start rounded-lg border border-dashed border-divider-regular px-2 text-xs text-text-placeholder ${open && 'bg-components-dropzone-bg-alt!'} `}
|
||||
>
|
||||
<div className="mr-1.5 flex h-5 w-5 items-center justify-center rounded-[5px] bg-background-default-dimmed">
|
||||
<RiAddLine aria-hidden className="size-3" />
|
||||
</div>
|
||||
<div className="flex items-center uppercase">{tip}</div>
|
||||
</Button>
|
||||
)
|
||||
},
|
||||
[nodesReadOnly, tip],
|
||||
const triggerElement = (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="large"
|
||||
className="bg-dropzone-bg hover:bg-dropzone-bg-hover relative w-full justify-start rounded-lg border border-dashed border-divider-regular px-2 text-xs text-text-placeholder data-popup-open:bg-components-dropzone-bg-alt!"
|
||||
>
|
||||
<div className="mr-1.5 flex h-5 w-5 items-center justify-center rounded-[5px] bg-background-default-dimmed">
|
||||
<RiAddLine aria-hidden className="size-3" />
|
||||
</div>
|
||||
<div className="flex items-center uppercase">{tip}</div>
|
||||
</Button>
|
||||
)
|
||||
|
||||
return (
|
||||
@ -83,7 +78,7 @@ const Add = ({ nodeId, nodeData, sourceHandle, isParallel, isFailBranch }: AddPr
|
||||
}}
|
||||
placement="top"
|
||||
sideOffset={0}
|
||||
trigger={renderTrigger}
|
||||
trigger={triggerElement}
|
||||
popupClassName="w-[328px]!"
|
||||
availableBlocksTypes={availableNextBlocks}
|
||||
/>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user