diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json index b52ee31d98e..d459c3fafe7 100644 --- a/oxlint-suppressions.json +++ b/oxlint-suppressions.json @@ -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 diff --git a/web/__mocks__/base-ui-dropdown-menu.tsx b/web/__mocks__/base-ui-dropdown-menu.tsx index 90ff259754a..d3b439f0dfc 100644 --- a/web/__mocks__/base-ui-dropdown-menu.tsx +++ b/web/__mocks__/base-ui-dropdown-menu.tsx @@ -12,10 +12,19 @@ type DropdownMenuProps = { onOpenChange?: (open: boolean) => void } -type DropdownMenuTriggerProps = React.HTMLAttributes & { +type TriggerHtmlProps = React.HTMLAttributes & { + '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 & { @@ -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) => { + 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> const childProps = (triggerElement.props ?? {}) as React.HTMLAttributes & { @@ -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), diff --git a/web/__mocks__/base-ui-popover.tsx b/web/__mocks__/base-ui-popover.tsx index 3c393ebd0b8..65324a56733 100644 --- a/web/__mocks__/base-ui-popover.tsx +++ b/web/__mocks__/base-ui-popover.tsx @@ -12,10 +12,18 @@ type PopoverProps = { onOpenChange?: (open: boolean) => void } -type PopoverTriggerProps = React.HTMLAttributes & { +type TriggerHtmlProps = React.HTMLAttributes & { + '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 & { @@ -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) => { + onClick?.(event) + if (event.defaultPrevented) return + onOpenChange(!open) + }, + } + return render(triggerProps, { open }) + } + const node = render ?? children if (React.isValidElement(node)) { diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/date-picker.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/date-picker.tsx index 4cfc061c273..e438634162f 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/date-picker.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/date-picker.tsx @@ -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 = ({ start, end, onStartChange, onEndChange }) => { const locale = useLocale() - const renderDate = useCallback( - ({ value, handleClickTrigger, isOpen }: TriggerProps) => { + const renderDate = useCallback>( + (props, _state, { value, handleClickTrigger }: TriggerProps) => { return (
{ + handleClickTrigger(event) + props.onClick?.(event) + }} > {value ? formatToLocalTime(value, locale, 'MMM D') : ''}
diff --git a/web/app/components/app-sidebar/snippet-info/dropdown.tsx b/web/app/components/app-sidebar/snippet-info/dropdown.tsx index 27289b7c14f..c5c0a1a0782 100644 --- a/web/app/components/app-sidebar/snippet-info/dropdown.tsx +++ b/web/app/components/app-sidebar/snippet-info/dropdown.tsx @@ -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 ( <> - + diff --git a/web/app/components/app/app-access-control/__tests__/access-control-item.spec.tsx b/web/app/components/app/app-access-control/__tests__/access-control-item.spec.tsx index 2bd51a9a2d2..c28207801ef 100644 --- a/web/app/components/app/app-access-control/__tests__/access-control-item.spec.tsx +++ b/web/app/components/app/app-access-control/__tests__/access-control-item.spec.tsx @@ -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) diff --git a/web/app/components/app/app-access-control/__tests__/access-control.spec.tsx b/web/app/components/app/app-access-control/__tests__/access-control.spec.tsx index 3e557b5f958..7a44ec44fbe 100644 --- a/web/app/components/app/app-access-control/__tests__/access-control.spec.tsx +++ b/web/app/components/app/app-access-control/__tests__/access-control.spec.tsx @@ -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')) diff --git a/web/app/components/app/app-access-control/access-control-item.tsx b/web/app/components/app/app-access-control/access-control-item.tsx index f4f5d3bb03c..358ce8bbd35 100644 --- a/web/app/components/app/app-access-control/access-control-item.tsx +++ b/web/app/components/app/app-access-control/access-control-item.tsx @@ -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} diff --git a/web/app/components/app/app-access-control/add-member-or-group-pop.tsx b/web/app/components/app/app-access-control/add-member-or-group-pop.tsx index fe229000335..cc2afcca131 100644 --- a/web/app/components/app/app-access-control/add-member-or-group-pop.tsx +++ b/web/app/components/app/app-access-control/add-member-or-group-pop.tsx @@ -297,19 +297,23 @@ function GroupItem({ group, subject }: GroupItemProps) { return (
- - -
-
-
-
- {group.name} - {group.groupSize} -
+ {(selected) => ( + <> + + +
+
+
+
+ {group.name} + {group.groupSize} +
+ + )}
)) @@ -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 }) => ( -
+ const renderTrigger = vi.fn((triggerProps, _state, { handleClickTrigger, handleClear }) => ( +
@@ -576,8 +584,8 @@ describe('DatePicker', () => { // Custom trigger describe('Custom Trigger', () => { it('should use renderTrigger when provided', () => { - const renderTrigger = vi.fn(({ handleClickTrigger }) => ( - )) @@ -589,8 +597,8 @@ describe('DatePicker', () => { }) it('should open picker when custom trigger is clicked', () => { - const renderTrigger = vi.fn(({ handleClickTrigger }) => ( - )) @@ -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) => ( + + )) + + render() + + 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 diff --git a/web/app/components/base/date-and-time-picker/date-picker/index.tsx b/web/app/components/base/date-and-time-picker/date-picker/index.tsx index f70ab90fb3b..0eeb609dd7a 100644 --- a/web/app/components/base/date-and-time-picker/date-picker/index.tsx +++ b/web/app/components/base/date-and-time-picker/date-picker/index.tsx @@ -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 ( { + 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 (
{ + handleClickTrigger(event) + props.onClick?.(event) + }} data-testid="date-picker-trigger" >
) - } + }} /> { }) it('should use renderTrigger when provided', () => { - const renderTrigger = vi.fn(({ inputElem, onClick }) => ( -
+ const renderTrigger = vi.fn((triggerProps, _state, { inputElem }) => ( +
{inputElem}
)) @@ -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 }) => ( + + )) + + render() + + 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() diff --git a/web/app/components/base/date-and-time-picker/time-picker/index.tsx b/web/app/components/base/date-and-time-picker/time-picker/index.tsx index 7587e08d78e..7b3cc9fe8be 100644 --- a/web/app/components/base/date-and-time-picker/time-picker/index.tsx +++ b/web/app/components/base/date-and-time-picker/time-picker/index.tsx @@ -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 = ( - - ) + return ( + + ) + } return ( { + const inputElem = renderInput(state.open) + if (renderTrigger) + return renderTrigger(props, state, { inputElem, onClick: handleClickTrigger }) + + return (
{ + handleClickTrigger(event) + props.onClick?.(event) + }} data-testid="time-picker-trigger" > {inputElem} @@ -240,8 +244,8 @@ const TimePicker = ({
) - } + }} /> , React.ReactElement> +type TriggerRenderProps = Parameters[0] +type TriggerState = Parameters[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 diff --git a/web/app/components/base/features/new-feature-panel/__tests__/follow-up-setting-modal.spec.tsx b/web/app/components/base/features/new-feature-panel/__tests__/follow-up-setting-modal.spec.tsx index 99268826c01..9eadafb3198 100644 --- a/web/app/components/base/features/new-feature-panel/__tests__/follow-up-setting-modal.spec.tsx +++ b/web/app/components/base/features/new-feature-panel/__tests__/follow-up-setting-modal.spec.tsx @@ -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() diff --git a/web/app/components/base/features/new-feature-panel/follow-up-setting-modal.tsx b/web/app/components/base/features/new-feature-panel/follow-up-setting-modal.tsx index 02492c58524..a900ed972d4 100644 --- a/web/app/components/base/features/new-feature-panel/follow-up-setting-modal.tsx +++ b/web/app/components/base/features/new-feature-panel/follow-up-setting-modal.tsx @@ -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={) + ((triggerProps, state) => ( + + )) const result = render( { }) it('should render trigger element', () => { - const trigger = (open: boolean) => ( - ) render( @@ -173,8 +180,13 @@ describe('FileFromLinkOrLocal', () => { }) it('should toggle open state when trigger is clicked', () => { - const trigger = (open: boolean) => ( - + const trigger: NonNullable['trigger']> = ( + triggerProps, + state, + ) => ( + ) render( diff --git a/web/app/components/base/file-uploader/file-from-link-or-local/index.tsx b/web/app/components/base/file-uploader/file-from-link-or-local/index.tsx index b8a28bc8e9b..7311bb7c601 100644 --- a/web/app/components/base/file-uploader/file-from-link-or-local/index.tsx +++ b/web/app/components/base/file-uploader/file-from-link-or-local/index.tsx @@ -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 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 ( - - + + { + (option: Option) => { return ( - ) - }, - [readonly, t], - ) + const renderTrigger = useCallback(() => { + return ( + + ) + }, [readonly, t]) return ( {readonly ? ( - renderTrigger(false) + renderTrigger() ) : ( { 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', () => { diff --git a/web/app/components/datasets/create/step-two/language-select/index.tsx b/web/app/components/datasets/create/step-two/language-select/index.tsx index 96a281807b0..122a1c58a1f 100644 --- a/web/app/components/datasets/create/step-two/language-select/index.tsx +++ b/web/app/components/datasets/create/step-two/language-select/index.tsx @@ -36,8 +36,7 @@ const LanguageSelect: FC = ({ 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', )} >  } /> diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/base/credential-selector/__tests__/index.spec.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/base/credential-selector/__tests__/index.spec.tsx index 761d5657fce..48bb5cdd385 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/base/credential-selector/__tests__/index.spec.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/base/credential-selector/__tests__/index.spec.tsx @@ -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() diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/base/credential-selector/index.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/base/credential-selector/index.tsx index b5b144647cf..ed516f2f52f 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/base/credential-selector/index.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/base/credential-selector/index.tsx @@ -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 ( - }> - - + ( +
+ +
+ )} + /> { const user = userEvent.setup() render() - 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() }) diff --git a/web/app/components/datasets/extra-info/api-access/index.tsx b/web/app/components/datasets/extra-info/api-access/index.tsx index 9d48c39b23c..ff9f01d4a28 100644 --- a/web/app/components/datasets/extra-info/api-access/index.tsx +++ b/web/app/components/datasets/extra-info/api-access/index.tsx @@ -20,13 +20,17 @@ const ApiAccess = ({ expand, apiEnabled }: ApiAccessProps) => {
+ render={(props, state) => ( + - } + )} /> { const user = userEvent.setup() render() - 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() diff --git a/web/app/components/datasets/extra-info/service-api/index.tsx b/web/app/components/datasets/extra-info/service-api/index.tsx index 43f2ef43f9a..f0cc583dea7 100644 --- a/web/app/components/datasets/extra-info/service-api/index.tsx +++ b/web/app/components/datasets/extra-info/service-api/index.tsx @@ -33,12 +33,16 @@ const ServiceApi = ({ apiBaseUrl }: ServiceApiProps) => {
+ render={(props, state) => ( +
- } + )} /> void onClear: () => void - renderTrigger: (args: TriggerArgs) => React.ReactNode + renderTrigger: ( + props: React.HTMLAttributes, + 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 (
{trigger} diff --git a/web/app/components/datasets/metadata/base/date-picker.tsx b/web/app/components/datasets/metadata/base/date-picker.tsx index 1f21629d431..27255c02607 100644 --- a/web/app/components/datasets/metadata/base/date-picker.tsx +++ b/web/app/components/datasets/metadata/base/date-picker.tsx @@ -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>( + (props, _state, { handleClickTrigger }: TriggerProps) => { const hasValue = Boolean(value) const triggerText = value ? formatTimestamp( @@ -44,9 +47,11 @@ const WrappedDatePicker = ({ className, label, value, onChange }: Props) => { return (
)} @@ -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() }) diff --git a/web/app/components/header/account-dropdown/index.tsx b/web/app/components/header/account-dropdown/index.tsx index cc887ab8025..a2ec1e14b65 100644 --- a/web/app/components/header/account-dropdown/index.tsx +++ b/web/app/components/header/account-dropdown/index.tsx @@ -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) { $['account.account'], { ns: 'common' }), })} /> diff --git a/web/app/components/header/account-setting/__tests__/workspace-role-checkbox-list.spec.tsx b/web/app/components/header/account-setting/__tests__/workspace-role-checkbox-list.spec.tsx index e9d6ba41c3a..146398085b1 100644 --- a/web/app/components/header/account-setting/__tests__/workspace-role-checkbox-list.spec.tsx +++ b/web/app/components/header/account-setting/__tests__/workspace-role-checkbox-list.spec.tsx @@ -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( + , + ) + + 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: { diff --git a/web/app/components/header/account-setting/access-rules-page/__tests__/access-rule-section.spec.tsx b/web/app/components/header/account-setting/access-rules-page/__tests__/access-rule-section.spec.tsx index 8fbd7054571..2f7b61b036d 100644 --- a/web/app/components/header/account-setting/access-rules-page/__tests__/access-rule-section.spec.tsx +++ b/web/app/components/header/account-setting/access-rules-page/__tests__/access-rule-section.spec.tsx @@ -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', () => { diff --git a/web/app/components/header/account-setting/access-rules-page/access-rule-row-menu.tsx b/web/app/components/header/account-setting/access-rules-page/access-rule-row-menu.tsx index 20fda9555ae..bbc216be397 100644 --- a/web/app/components/header/account-setting/access-rules-page/access-rule-row-menu.tsx +++ b/web/app/components/header/account-setting/access-rules-page/access-rule-row-menu.tsx @@ -78,7 +78,7 @@ const AccessRuleRowMenu = ({ rule, onView, onEdit }: AccessRuleRowMenuProps) => render={ $['operation.moreActions'], { ns: 'common' })} /> } diff --git a/web/app/components/header/account-setting/api-based-extension-page/__tests__/selector.spec.tsx b/web/app/components/header/account-setting/api-based-extension-page/__tests__/selector.spec.tsx index 75f0b4267cc..78d91d333cf 100644 --- a/web/app/components/header/account-setting/api-based-extension-page/__tests__/selector.spec.tsx +++ b/web/app/components/header/account-setting/api-based-extension-page/__tests__/selector.spec.tsx @@ -93,11 +93,18 @@ describe('ApiBasedExtensionSelector', () => { it('should open dropdown when clicked', async () => { // Act render() + 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() diff --git a/web/app/components/header/account-setting/api-based-extension-page/selector.tsx b/web/app/components/header/account-setting/api-based-extension-page/selector.tsx index 925b6526b41..11df6400e54 100644 --- a/web/app/components/header/account-setting/api-based-extension-page/selector.tsx +++ b/web/app/components/header/account-setting/api-based-extension-page/selector.tsx @@ -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 <> + render={(props, state) => ( +
@@ -62,13 +70,16 @@ export function ApiBasedExtensionSelector({ value, onChange }: ApiBasedExtension ns: 'common', })}
)} - } + )} /> { pluginID={detail.plugin_id} currentVersion={detail.version} onSelect={handleVersionSelect} - trigger={ + trigger={() => ( { hasRedCornerMark={hasNewVersion} uppercase={false} /> - } + )} /> )} {canUpdatePlugin && (hasNewVersion || isFromGitHub) && ( diff --git a/web/app/components/header/account-setting/model-provider-page/__tests__/index.spec.tsx b/web/app/components/header/account-setting/model-provider-page/__tests__/index.spec.tsx index 5f0fd4805ef..1c11ae83740 100644 --- a/web/app/components/header/account-setting/model-provider-page/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/__tests__/index.spec.tsx @@ -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, + state: { open: boolean }, + params: { + inputElem: ReactNode + onClick: () => void + }, + ) => ReactNode }) => { const displayValue = typeof value === 'string' ? value : value?.format('HH:mm') return (
- {renderTrigger({ - inputElem: {displayValue}, - onClick: vi.fn(), - isOpen: false, - })} + {renderTrigger( + {}, + { open: false }, + { + inputElem: {displayValue}, + onClick: vi.fn(), + }, + )}
} + render={(props, state) => ( +
+ {renderTrigger(state.open)} +
+ )} /> { />, ) + 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() }) diff --git a/web/app/components/header/account-setting/model-provider-page/model-auth/authorized/index.tsx b/web/app/components/header/account-setting/model-provider-page/model-auth/authorized/index.tsx index 6a731210bdc..ea37e514546 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-auth/authorized/index.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-auth/authorized/index.tsx @@ -177,11 +177,14 @@ const Authorized = ({ - {renderTrigger(mergedIsOpen)} + render={(props, state) => ( +
+ {renderTrigger(state.open)}
- } + )} onClick={handleTriggerClick} /> { />, ) + 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')) diff --git a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/index.tsx b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/index.tsx index e730f48a868..068d962988b 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/index.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/index.tsx @@ -126,20 +126,24 @@ const ModelParameterModal: FC = ({ > {renderTrigger ? ( ( - } + )} /> ) : (
diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/provider-card-actions.spec.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/provider-card-actions.spec.tsx index 1266c7d9058..8a68107c8f4 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/provider-card-actions.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/provider-card-actions.spec.tsx @@ -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 }) => (
- {trigger} + {trigger(false)}
- } + )} /> { renderWithQueryClient() - 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' })) diff --git a/web/app/components/plugins/plugin-detail-panel/app-selector/index.tsx b/web/app/components/plugins/plugin-detail-panel/app-selector/index.tsx index f36b68c8064..053ece8afae 100644 --- a/web/app/components/plugins/plugin-detail-panel/app-selector/index.tsx +++ b/web/app/components/plugins/plugin-detail-panel/app-selector/index.tsx @@ -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({ $['appSelector.label'], { ns: 'app' })} disabled={disabled} - render={ - + )} + /> ( - } + )} /> )} diff --git a/web/app/components/plugins/plugin-detail-panel/model-selector/__tests__/index.spec.tsx b/web/app/components/plugins/plugin-detail-panel/model-selector/__tests__/index.spec.tsx index 06d103e6eaf..456f3495364 100644 --- a/web/app/components/plugins/plugin-detail-panel/model-selector/__tests__/index.spec.tsx +++ b/web/app/components/plugins/plugin-detail-panel/model-selector/__tests__/index.spec.tsx @@ -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(
Custom
) const value = { provider: 'openai', model: 'gpt-4' } @@ -352,7 +352,8 @@ describe('ModelParameterModal', () => { // Act render() - // 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', () => { diff --git a/web/app/components/plugins/plugin-detail-panel/model-selector/index.tsx b/web/app/components/plugins/plugin-detail-panel/model-selector/index.tsx index 5929f4a455d..13e460071f4 100644 --- a/web/app/components/plugins/plugin-detail-panel/model-selector/index.tsx +++ b/web/app/components/plugins/plugin-detail-panel/model-selector/index.tsx @@ -224,14 +224,18 @@ const ModelParameterModal: FC = ({
) : ( ( - } + )} /> )} { return {children} } - const PopoverTrigger = ({ render }: { render: React.ReactNode }) => { + type TriggerProps = React.HTMLAttributes & { + '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
setOpen(!open)}>{render}
+ const props: TriggerProps = { + 'data-testid': 'popover-trigger', + 'data-popup-open': open ? '' : undefined, + onClick: () => setOpen(!open), + } + + if (typeof render === 'function') return render(props, { open }) + + return
{render}
} const PopoverContent = ({ children }: { children: React.ReactNode }) => { @@ -112,8 +131,12 @@ describe('SubscriptionSelectorEntry', () => { it('should render placeholder when open without selection', () => { render() + 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() }) diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/selector-entry.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/selector-entry.tsx index 9760e65c182..73d183f65d2 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/selector-entry.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/selector-entry.tsx @@ -106,11 +106,11 @@ export const SubscriptionSelectorEntry = ({ return ( - + render={(props, state) => ( +
+
- } + )} /> { onClick, render, }: { - children: React.ReactNode + children?: React.ReactNode onClick?: React.MouseEventHandler - render?: React.ReactElement + render?: + | React.ReactElement + | (( + props: React.HTMLAttributes & { + 'data-testid'?: string + 'data-popup-open'?: string + }, + state: { open: boolean }, + ) => React.ReactElement) }) => { const { isOpen, setOpen } = useDropdownMenuContext() const handleClick = (e: React.MouseEvent) => { @@ -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') }) diff --git a/web/app/components/plugins/plugin-page/install-plugin-dropdown.tsx b/web/app/components/plugins/plugin-page/install-plugin-dropdown.tsx index 7373efed4b5..f1b5b63c255 100644 --- a/web/app/components/plugins/plugin-page/install-plugin-dropdown.tsx +++ b/web/app/components/plugins/plugin-page/install-plugin-dropdown.tsx @@ -160,8 +160,9 @@ const InstallPluginDropdown = ({ accept={SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS} /> ( + )} + /> } + render={(props, state) => ( +
+ {}} + /> +
+ )} disabled={!canOpenMenu} - > - {}} - /> -
+ /> ({ onChange: (v: unknown) => void onClear: () => void title?: string - renderTrigger: (params: { - inputElem: React.ReactNode - onClick: () => void - isOpen: boolean - }) => React.ReactNode + renderTrigger: ( + props: React.HTMLAttributes, + state: { open: boolean }, + params: { inputElem: React.ReactNode; onClick: () => void }, + ) => React.ReactNode }) => { const inputElem = {value.format('HH:mm')} return (
- {renderTrigger({ - inputElem, - onClick: () => {}, - isOpen: false, - })} + {renderTrigger({}, { open: false }, { inputElem, onClick: () => {} })}
+ )} + /> = ({ BlockEnum.TriggerWebhook, BlockEnum.TriggerPlugin, ]} - trigger={() => ( + trigger={ @@ -63,7 +63,7 @@ const StartNodeSelectionPanel: FC = ({ description={t(($) => $['onboarding.triggerDescription'], { ns: 'workflow' })} onClick={() => setShowTriggerSelector(true)} /> - )} + } />
) diff --git a/web/app/components/workflow/block-selector/__tests__/index.spec.tsx b/web/app/components/workflow/block-selector/__tests__/index.spec.tsx index 3df1535d286..8c3fea6a174 100644 --- a/web/app/components/workflow/block-selector/__tests__/index.spec.tsx +++ b/web/app/components/workflow/block-selector/__tests__/index.spec.tsx @@ -107,8 +107,10 @@ describe('BlockSelector', () => { onSelect={onSelect} blocks={[createBlock(BlockEnum.LLM, 'LLM'), createBlock(BlockEnum.End, 'End')]} availableBlocksTypes={[BlockEnum.LLM, BlockEnum.End]} - trigger={(open) => ( - + trigger={(props, state) => ( + )} />, ) @@ -155,8 +157,10 @@ describe('BlockSelector', () => { blocks={[createBlock(BlockEnum.LLM, 'LLM')]} availableBlocksTypes={[BlockEnum.LLM, BlockEnum.Start]} showStartTab - trigger={(open) => ( - + trigger={(props, state) => ( + )} />, ) @@ -192,7 +196,7 @@ describe('BlockSelector', () => { blocks={[createBlock(BlockEnum.LLM, 'LLM')]} availableBlocksTypes={[BlockEnum.LLM, BlockEnum.Start]} showStartTab - trigger={() => } + trigger={} />, ) @@ -218,8 +222,10 @@ describe('BlockSelector', () => { blocks={[createBlock(BlockEnum.LLM, 'LLM')]} availableBlocksTypes={[BlockEnum.LLM, BlockEnum.Start]} showStartTab - trigger={(open) => ( - + trigger={(props, state) => ( + )} />, ) @@ -255,8 +261,10 @@ describe('BlockSelector', () => { onSelect={vi.fn()} blocks={[createBlock(BlockEnum.LLM, 'LLM')]} availableBlocksTypes={[BlockEnum.LLM]} - trigger={(open) => ( - + trigger={(props, state) => ( + )} />, ) @@ -287,7 +295,7 @@ describe('BlockSelector', () => { onOpenChange={setOpen} onSelect={vi.fn()} blocks={[createBlock(BlockEnum.LLM, 'LLM')]} - trigger={() => } + trigger={} /> ) } @@ -314,7 +322,7 @@ describe('BlockSelector', () => { } + trigger={} />, ) @@ -339,11 +347,11 @@ describe('BlockSelector', () => { onSelect={vi.fn()} blocks={[createBlock(BlockEnum.LLM, 'LLM')]} availableBlocksTypes={[BlockEnum.LLM]} - trigger={() => ( + trigger={ - )} + } />, ) @@ -369,7 +377,7 @@ describe('BlockSelector', () => { onSelect={vi.fn()} blocks={[createBlock(BlockEnum.LLM, 'LLM')]} availableBlocksTypes={[BlockEnum.LLM]} - trigger={() => } + trigger={} />, ) @@ -388,7 +396,7 @@ describe('BlockSelector', () => { onSelect={vi.fn()} blocks={[createBlock(BlockEnum.LLM, 'LLM')]} availableBlocksTypes={[BlockEnum.LLM]} - trigger={() => } + trigger={} />, ) diff --git a/web/app/components/workflow/block-selector/index.tsx b/web/app/components/workflow/block-selector/index.tsx index 15a11c87171..308d5f1b5f1 100644 --- a/web/app/components/workflow/block-selector/index.tsx +++ b/web/app/components/workflow/block-selector/index.tsx @@ -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 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({ ) : ( @@ -136,7 +136,7 @@ function BlockSelector({
diff --git a/web/app/components/workflow/header/checklist/index.tsx b/web/app/components/workflow/header/checklist/index.tsx index 2b6511d5e8e..3d546f46672 100644 --- a/web/app/components/workflow/header/checklist/index.tsx +++ b/web/app/components/workflow/header/checklist/index.tsx @@ -59,12 +59,13 @@ const WorkflowChecklist = ({ disabled, showGoTo = true, onItemClick }: WorkflowC return ( !disabled && setOpen(newOpen)}> @@ -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} > diff --git a/web/app/components/workflow/header/view-workflow-history.tsx b/web/app/components/workflow/header/view-workflow-history.tsx index 0b242ae6f52..816d7e3a44a 100644 --- a/web/app/components/workflow/header/view-workflow-history.tsx +++ b/web/app/components/workflow/header/view-workflow-history.tsx @@ -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 diff --git a/web/app/components/workflow/node-actions-menu/__tests__/details.spec.tsx b/web/app/components/workflow/node-actions-menu/__tests__/details.spec.tsx index 1bedad990da..03430534151 100644 --- a/web/app/components/workflow/node-actions-menu/__tests__/details.spec.tsx +++ b/web/app/components/workflow/node-actions-menu/__tests__/details.spec.tsx @@ -31,7 +31,7 @@ vi.mock('@/app/components/workflow/block-selector', () => ({ isolateKeyboardEvents, }: any) => (
-
{trigger()}
+
{typeof trigger === 'function' ? trigger({}, { open: false }) : trigger}
{`available:${(availableBlocksTypes || []).join(',')}`}
{`show-start:${String(showStartTab)}`}
{`ignore:${(ignoreNodeIds || []).join(',')}`}
diff --git a/web/app/components/workflow/node-actions-menu/change-block-menu-trigger.tsx b/web/app/components/workflow/node-actions-menu/change-block-menu-trigger.tsx index 51f60f4bbff..672e60afec1 100644 --- a/web/app/components/workflow/node-actions-menu/change-block-menu-trigger.tsx +++ b/web/app/components/workflow/node-actions-menu/change-block-menu-trigger.tsx @@ -60,21 +60,19 @@ export function ChangeBlockMenuTrigger({ [handleNodeChange, nodeId, sourceHandle], ) - const renderTrigger = useCallback(() => { - return ( - - ) - }, [t]) + const triggerElement = ( + + ) return ( 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', () => ({
diff --git a/web/app/components/workflow/nodes/_base/components/next-step/add.tsx b/web/app/components/workflow/nodes/_base/components/next-step/add.tsx index e478f8ecd46..297872e5ad9 100644 --- a/web/app/components/workflow/nodes/_base/components/next-step/add.tsx +++ b/web/app/components/workflow/nodes/_base/components/next-step/add.tsx @@ -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 ( - - ) - }, - [nodesReadOnly, tip], + const triggerElement = ( + ) 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} /> diff --git a/web/app/components/workflow/nodes/_base/components/next-step/operator.tsx b/web/app/components/workflow/nodes/_base/components/next-step/operator.tsx index 2b1da42a8f8..5fb68bfbb29 100644 --- a/web/app/components/workflow/nodes/_base/components/next-step/operator.tsx +++ b/web/app/components/workflow/nodes/_base/components/next-step/operator.tsx @@ -35,13 +35,11 @@ const ChangeItem = ({ data, nodeId, sourceHandle }: ChangeItemProps) => { [nodeId, sourceHandle, handleNodeChange], ) - const renderTrigger = useCallback(() => { - return ( - - ) - }, [t]) + const triggerElement = ( + + ) return ( { placement="top-end" sideOffset={6} alignOffset={8} - trigger={renderTrigger} + trigger={triggerElement} popupClassName="w-[328px]!" availableBlocksTypes={intersection(availablePrevBlocks, availableNextBlocks).filter( (item) => item !== nodeCatalogType, diff --git a/web/app/components/workflow/nodes/_base/components/node-handle.tsx b/web/app/components/workflow/nodes/_base/components/node-handle.tsx index 324ea9d7b0c..955e5eb2f87 100644 --- a/web/app/components/workflow/nodes/_base/components/node-handle.tsx +++ b/web/app/components/workflow/nodes/_base/components/node-handle.tsx @@ -106,12 +106,12 @@ export const NodeTargetHandle = memo( nextNodeTargetHandle: handleId, }} placement="left" - triggerClassName={(open) => ` + triggerClassName={` absolute left-0 top-0 opacity-0 pointer-events-none transition-opacity duration-150 ${nodeSelectorClassName} group-hover:opacity-100 ${data.selected && 'opacity-100'} - ${open && 'opacity-100'} + data-popup-open:opacity-100 `} availableBlocksTypes={availablePrevBlocks} /> @@ -244,12 +244,12 @@ export const NodeSourceHandle = memo( prevNodeId: id, prevNodeSourceHandle: handleId, }} - triggerClassName={(open) => ` + triggerClassName={` absolute top-0 left-0 opacity-0 pointer-events-none transition-opacity duration-150 ${nodeSelectorClassName} group-hover:opacity-100 ${data.selected && 'opacity-100'} - ${open && 'opacity-100'} + data-popup-open:opacity-100 `} availableBlocksTypes={availableNextBlocks} /> diff --git a/web/app/components/workflow/nodes/_base/components/switch-plugin-version.tsx b/web/app/components/workflow/nodes/_base/components/switch-plugin-version.tsx index 64857e8cb64..9df1e2d14a9 100644 --- a/web/app/components/workflow/nodes/_base/components/switch-plugin-version.tsx +++ b/web/app/components/workflow/nodes/_base/components/switch-plugin-version.tsx @@ -123,9 +123,9 @@ export const SwitchPluginVersion: FC = (props) => { }) setIsShowUpdateModal(true) }} - trigger={ + trigger={(open) => ( @@ -135,7 +135,7 @@ export const SwitchPluginVersion: FC = (props) => { } hasRedCornerMark={true} /> - } + )} /> )}
diff --git a/web/app/components/workflow/nodes/assigner/components/operation-selector.tsx b/web/app/components/workflow/nodes/assigner/components/operation-selector.tsx index 5cc1a55800b..671115643d9 100644 --- a/web/app/components/workflow/nodes/assigner/components/operation-selector.tsx +++ b/web/app/components/workflow/nodes/assigner/components/operation-selector.tsx @@ -58,9 +58,7 @@ const OperationSelector: FC = ({ disabled={disabled} className={cn( 'group flex items-center gap-0.5 rounded-lg bg-components-input-bg-normal px-2 py-1 data-popup-open:bg-state-base-hover-alt', - disabled - ? 'cursor-not-allowed bg-components-input-bg-disabled!' - : 'cursor-pointer hover:bg-state-base-hover-alt', + 'cursor-pointer hover:bg-state-base-hover-alt data-disabled:cursor-not-allowed data-disabled:bg-components-input-bg-disabled! data-disabled:hover:bg-components-input-bg-disabled!', className, )} > diff --git a/web/app/components/workflow/nodes/data-source-empty/__tests__/index.spec.tsx b/web/app/components/workflow/nodes/data-source-empty/__tests__/index.spec.tsx index 5ff6179ec86..8bde043261a 100644 --- a/web/app/components/workflow/nodes/data-source-empty/__tests__/index.spec.tsx +++ b/web/app/components/workflow/nodes/data-source-empty/__tests__/index.spec.tsx @@ -12,15 +12,9 @@ vi.mock('../hooks', () => ({ })) vi.mock('@/app/components/workflow/block-selector', () => ({ - default: ({ - onSelect, - trigger, - }: { - onSelect: OnSelectBlock - trigger: ((open?: boolean) => ReactNode) | ReactNode - }) => ( + default: ({ onSelect, trigger }: { onSelect: OnSelectBlock; trigger: ReactNode }) => (
- {typeof trigger === 'function' ? trigger(false) : trigger} + {trigger} - ) - }, [t]) + const triggerElement = ( + + ) return (
{
({ availableBlocksTypes = [], disabled, }: { - trigger?: (open: boolean) => ReactNode + trigger?: + | ReactNode + | ((props: React.HTMLAttributes, state: { open: boolean }) => ReactNode) onSelect?: (type: BlockEnum) => void availableBlocksTypes?: BlockEnum[] disabled?: boolean }) => (
- {trigger ?
{trigger(false)}
: null} + {trigger ? ( +
{typeof trigger === 'function' ? trigger({}, { open: false }) : trigger}
+ ) : null} - ) - }, - [nodesReadOnly, t], + const triggerElement = ( + ) return ( @@ -65,7 +59,7 @@ const AddBlock = ({ iterationNodeData }: AddBlockProps) => { prevNodeId: iterationNodeData.start_node_id, prevNodeSourceHandle: 'source', }} - trigger={renderTriggerElement} + trigger={triggerElement} popupClassName="min-w-[256px]!" availableBlocksTypes={availableNextBlocks} /> diff --git a/web/app/components/workflow/nodes/knowledge-base/components/retrieval-setting/search-method-option.tsx b/web/app/components/workflow/nodes/knowledge-base/components/retrieval-setting/search-method-option.tsx index 8c839b550a7..b67c0f03ff1 100644 --- a/web/app/components/workflow/nodes/knowledge-base/components/retrieval-setting/search-method-option.tsx +++ b/web/app/components/workflow/nodes/knowledge-base/components/retrieval-setting/search-method-option.tsx @@ -154,7 +154,7 @@ function SearchMethodRadioCard({ disabled={readonly} className={cn( 'relative flex w-full rounded-t-xl p-2 text-left outline-hidden focus-visible:ring-1 focus-visible:ring-components-input-border-active', - readonly ? 'cursor-not-allowed' : 'cursor-pointer', + 'cursor-pointer data-disabled:cursor-not-allowed', )} > {getSearchMethodEffect(option.effectColor, isActive)} @@ -205,7 +205,7 @@ function HybridSearchModeRadioCard({ 'w-full rounded-xl border border-components-option-card-option-border bg-components-option-card-option-bg p-3 text-left outline-hidden transition-colors', 'data-checked:border-[1.5px] data-checked:bg-components-option-card-option-selected-bg', 'focus-visible:ring-1 focus-visible:ring-components-input-border-active', - readonly ? 'cursor-not-allowed' : 'cursor-pointer hover:shadow-xs', + 'cursor-pointer hover:shadow-xs data-disabled:cursor-not-allowed data-disabled:hover:shadow-none', )} >
diff --git a/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-date.tsx b/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-date.tsx index c95ee63fbea..31632773f96 100644 --- a/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-date.tsx +++ b/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-date.tsx @@ -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' @@ -27,8 +30,8 @@ const ConditionDate = ({ value, onChange }: ConditionDateProps) => { [onChange], ) - const renderTrigger = useCallback( - ({ handleClickTrigger }: TriggerProps) => { + const renderTrigger = useCallback>( + (props, _state, { handleClickTrigger }: TriggerProps) => { const hasValue = Boolean(value) const triggerText = value ? dayjs(value * 1000) @@ -37,7 +40,7 @@ const ConditionDate = ({ value, onChange }: ConditionDateProps) => { : t(($) => $['nodes.knowledgeRetrieval.metadata.panel.datePlaceholder'], { ns: 'workflow' }) return ( -
+
diff --git a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/edit-card/__tests__/type-selector.spec.tsx b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/edit-card/__tests__/type-selector.spec.tsx new file mode 100644 index 00000000000..03b11571809 --- /dev/null +++ b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/edit-card/__tests__/type-selector.spec.tsx @@ -0,0 +1,45 @@ +import type { TypeItem } from '../type-selector' +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { useState } from 'react' +import { Type } from '../../../../../types' +import TypeSelector from '../type-selector' + +const items: TypeItem[] = [ + { value: Type.string, text: 'String' }, + { value: Type.number, text: 'Number' }, +] + +function StatefulTypeSelector() { + const [currentValue, setCurrentValue] = useState(Type.string) + return ( + setCurrentValue(item.value as Type)} + /> + ) +} + +describe('TypeSelector', () => { + it('renders the check from Select item state and updates it after selection', async () => { + const user = userEvent.setup() + render() + + await user.click(screen.getByRole('combobox')) + + const stringOption = screen.getByRole('option', { name: 'String' }) + const numberOption = screen.getByRole('option', { name: 'Number' }) + expect(stringOption).toHaveAttribute('data-selected') + expect(stringOption.querySelector('svg')).toBeInTheDocument() + expect(numberOption).not.toHaveAttribute('data-selected') + expect(numberOption.querySelector('svg')).not.toBeInTheDocument() + + await user.click(numberOption) + await user.click(screen.getByRole('combobox')) + + expect(screen.getByRole('option', { name: 'String' })).not.toHaveAttribute('data-selected') + expect(screen.getByRole('option', { name: 'Number' })).toHaveAttribute('data-selected') + expect(screen.getByRole('option', { name: 'Number' }).querySelector('svg')).toBeInTheDocument() + }) +}) diff --git a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/edit-card/type-selector.tsx b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/edit-card/type-selector.tsx index 5d3f7edf529..7c2a9651620 100644 --- a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/edit-card/type-selector.tsx +++ b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/edit-card/type-selector.tsx @@ -1,6 +1,5 @@ import type { FC } from 'react' import type { ArrayType, Type } from '../../../../types' -import { cn } from '@langgenius/dify-ui/cn' import { Select, SelectContent, @@ -38,12 +37,7 @@ const TypeSelector: FC = ({ items, currentValue, onSelect, po if (selected) onSelect(selected) }} > - + = ({ items, currentValue, onSelect, po popupClassName="w-40 rounded-xl border-[0.5px] p-1 shadow-lg shadow-shadow-shadow-5" listClassName="p-0" > - {items.map((item) => { - const isSelected = item.value === currentValue - return ( - - key={item.value} - value={item.value} - className="gap-x-1 rounded-lg px-2 py-1" - > - - {item.text} - - {isSelected && } - - - ) - })} + {items.map((item) => ( + + key={item.value} + value={item.value} + className="gap-x-1 rounded-lg px-2 py-1" + render={(props, state) => ( +
+ + {item.text} + + {state.selected && } + +
+ )} + /> + ))}
) diff --git a/web/app/components/workflow/nodes/loop/add-block.tsx b/web/app/components/workflow/nodes/loop/add-block.tsx index ae761cf741d..1046a7121d1 100644 --- a/web/app/components/workflow/nodes/loop/add-block.tsx +++ b/web/app/components/workflow/nodes/loop/add-block.tsx @@ -1,7 +1,6 @@ import type { LoopNodeType } from './types' import type { OnSelectBlock } from '@/app/components/workflow/types' import { Button } from '@langgenius/dify-ui/button' -import { cn } from '@langgenius/dify-ui/cn' import { RiAddLine } from '@remixicon/react' import { memo, useCallback } from 'react' import { useTranslation } from 'react-i18next' @@ -37,20 +36,15 @@ const AddBlock = ({ loopNodeData }: AddBlockProps) => { [handleNodeAdd, loopNodeData.start_node_id], ) - const renderTriggerElement = useCallback( - (open: boolean) => { - return ( - - ) - }, - [nodesReadOnly, t], + const triggerElement = ( + ) return ( @@ -65,7 +59,7 @@ const AddBlock = ({ loopNodeData }: AddBlockProps) => { prevNodeId: loopNodeData.start_node_id, prevNodeSourceHandle: 'source', }} - trigger={renderTriggerElement} + trigger={triggerElement} popupClassName="min-w-[256px]!" availableBlocksTypes={availableNextBlocks} /> diff --git a/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/import-from-tool.tsx b/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/import-from-tool.tsx index a4265c3589c..0334d73a4d8 100644 --- a/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/import-from-tool.tsx +++ b/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/import-from-tool.tsx @@ -8,7 +8,6 @@ import type { } from '@/app/components/workflow/block-selector/types' import type { BlockEnum } from '@/app/components/workflow/types' import { Button } from '@langgenius/dify-ui/button' -import { cn } from '@langgenius/dify-ui/cn' import { memo, useCallback } from 'react' import { useTranslation } from 'react-i18next' import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks' @@ -69,19 +68,14 @@ const ImportFromTool: FC = ({ onImport }) => { [buildInTools, customTools, language, onImport, workflowTools], ) - const renderTrigger = useCallback( - (open: boolean) => { - return ( - - ) - }, - [t], + const triggerElement = ( + ) return ( @@ -89,7 +83,7 @@ const ImportFromTool: FC = ({ onImport }) => { placement="bottom-end" sideOffset={4} alignOffset={52} - trigger={renderTrigger} + trigger={triggerElement} onSelect={handleSelectTool} standalonePanel={TabType.Tools} /> diff --git a/web/app/components/workflow/nodes/variable-assigner/components/add-variable/__tests__/index.spec.tsx b/web/app/components/workflow/nodes/variable-assigner/components/add-variable/__tests__/index.spec.tsx index f588b6acf43..c628024693f 100644 --- a/web/app/components/workflow/nodes/variable-assigner/components/add-variable/__tests__/index.spec.tsx +++ b/web/app/components/workflow/nodes/variable-assigner/components/add-variable/__tests__/index.spec.tsx @@ -52,8 +52,14 @@ describe('variable-assigner/add-variable', () => { />, ) - const trigger = container.querySelector('div[class*="group/addvariable"]') - fireEvent.click(trigger as HTMLElement) + const triggerButton = container.querySelector('button')! + const triggerVisual = triggerButton.firstElementChild! + expect(triggerButton).not.toHaveAttribute('data-popup-open') + + fireEvent.click(triggerButton) + + expect(triggerButton).toHaveAttribute('data-popup-open', '') + expect(triggerVisual).toHaveClass('bg-primary-600!') expect( screen.getByText('workflow.nodes.variableAssigner.setAssignVariable'), diff --git a/web/app/components/workflow/nodes/variable-assigner/components/add-variable/index.tsx b/web/app/components/workflow/nodes/variable-assigner/components/add-variable/index.tsx index ff546beffbf..1d8d7757d7c 100644 --- a/web/app/components/workflow/nodes/variable-assigner/components/add-variable/index.tsx +++ b/web/app/components/workflow/nodes/variable-assigner/components/add-variable/index.tsx @@ -34,26 +34,30 @@ const AddVariable = ({
+ render={(props, state) => ( + - } + )} /> { render={