feat: add dify-ui collapsible primitive and refactor workflow collapse usage (#37276)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
yyh 2026-06-10 16:59:33 +08:00 committed by GitHub
parent fb70ebb8f8
commit 0d8f7c41de
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 650 additions and 225 deletions

View File

@ -3529,11 +3529,6 @@
"count": 5
}
},
"web/app/components/workflow/nodes/_base/components/collapse/index.tsx": {
"no-barrel-files/no-barrel-files": {
"count": 1
}
},
"web/app/components/workflow/nodes/_base/components/editor/code-editor/editor-support-vars.tsx": {
"ts/no-explicit-any": {
"count": 6

View File

@ -47,7 +47,7 @@ Importing from `@langgenius/dify-ui` (no subpath) is intentionally not supported
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- |
| Actions | `./button` | Design-system CTA primitive with `cva` variants. |
| Controls | `./segmented-control` | SegmentedControl for mode, filter, and view selection. |
| Display | `./kbd` | Keyboard input and shortcut keycap primitives. |
| Display | `./collapsible`, `./kbd` | Collapsible disclosure primitive; keyboard input and shortcut keycap primitives. |
| Feedback | `./meter`, `./toast` | Meter is inline status; Toast owns the `z-60` layer. |
| Form | `./form`, `./field`, `./fieldset`, `./input`, `./textarea`, `./checkbox`, `./checkbox-group`, `./radio`, `./radio-group`, `./number-field`, `./select`, `./slider`, `./switch` | Native form boundary, field semantics, and controls. |
| Layout | `./scroll-area` | Custom-styled scrollbar over the host viewport. |

View File

@ -37,6 +37,10 @@
"types": "./src/combobox/index.tsx",
"import": "./src/combobox/index.tsx"
},
"./collapsible": {
"types": "./src/collapsible/index.tsx",
"import": "./src/collapsible/index.tsx"
},
"./context-menu": {
"types": "./src/context-menu/index.tsx",
"import": "./src/context-menu/index.tsx"

View File

@ -0,0 +1,89 @@
import { render } from 'vitest-browser-react'
import {
CollapsiblePanel,
CollapsibleRoot,
CollapsibleTrigger,
} from '../index'
const asHTMLElement = (element: HTMLElement | SVGElement) => element as HTMLElement
describe('Collapsible wrappers', () => {
it('renders the Base UI anatomy with an accessible trigger', async () => {
const screen = await render(
<CollapsibleRoot defaultOpen data-testid="collapsible-root">
<CollapsibleTrigger>Recovery keys</CollapsibleTrigger>
<CollapsiblePanel>Panel content</CollapsiblePanel>
</CollapsibleRoot>,
)
await expect.element(screen.getByTestId('collapsible-root')).toBeInTheDocument()
await expect.element(screen.getByRole('button', { name: 'Recovery keys' })).toHaveAttribute('data-panel-open', '')
await expect.element(screen.getByText('Panel content')).toBeInTheDocument()
})
it('toggles open state through the trigger without caller-owned state', async () => {
const screen = await render(
<CollapsibleRoot>
<CollapsibleTrigger>Toggle section</CollapsibleTrigger>
<CollapsiblePanel>Hidden content</CollapsiblePanel>
</CollapsibleRoot>,
)
const trigger = screen.getByRole('button', { name: 'Toggle section' })
await expect.element(trigger).not.toHaveAttribute('data-panel-open')
asHTMLElement(trigger.element()).click()
await expect.element(trigger).toHaveAttribute('data-panel-open', '')
await expect.element(screen.getByText('Hidden content')).toBeInTheDocument()
})
it('forwards className to every compound part', async () => {
const screen = await render(
<CollapsibleRoot defaultOpen className="custom-root">
<CollapsibleTrigger className="custom-trigger">Custom</CollapsibleTrigger>
<CollapsiblePanel className="custom-panel">Custom panel</CollapsiblePanel>
</CollapsibleRoot>,
)
await expect.element(screen.getByRole('button', { name: 'Custom' })).toHaveClass('custom-trigger')
expect(screen.getByText('Custom panel').element()).toHaveClass('custom-panel')
expect(screen.container.querySelector('.custom-root')).toBeInTheDocument()
})
it('passes Base UI panel props through to the panel', async () => {
const screen = await render(
<CollapsibleRoot defaultOpen>
<CollapsibleTrigger>Styled trigger</CollapsibleTrigger>
<CollapsiblePanel keepMounted>Styled panel</CollapsiblePanel>
</CollapsibleRoot>,
)
await expect.element(screen.getByRole('button', { name: 'Styled trigger' })).toHaveAttribute('data-panel-open', '')
await expect.element(screen.getByText('Styled panel')).toBeInTheDocument()
})
it('applies Dify disclosure defaults without a pressed active style', async () => {
const screen = await render(
<CollapsibleRoot defaultOpen>
<CollapsibleTrigger>Styled trigger</CollapsibleTrigger>
<CollapsiblePanel>Styled panel</CollapsiblePanel>
</CollapsibleRoot>,
)
const trigger = screen.getByRole('button', { name: 'Styled trigger' }).element()
const panel = screen.getByText('Styled panel').element()
expect(trigger).toHaveClass(
'hover:not-data-disabled:bg-components-panel-on-panel-item-bg-hover',
'focus-visible:ring-2',
'focus-visible:ring-state-accent-solid',
'data-panel-open:text-text-primary',
)
expect(trigger.className).not.toContain('active:')
expect(panel).toHaveClass(
'h-(--collapsible-panel-height)',
'data-ending-style:h-0',
'data-starting-style:h-0',
)
})
})

View File

@ -0,0 +1,183 @@
import type { Meta, StoryObj } from '@storybook/react-vite'
import * as React from 'react'
import {
CollapsiblePanel,
CollapsibleRoot,
CollapsibleTrigger,
} from '.'
import { cn } from '../cn'
const meta = {
title: 'Base/UI/Collapsible',
component: CollapsibleRoot,
parameters: {
layout: 'centered',
docs: {
description: {
component: 'Unstyled Base UI Collapsible primitive. The examples mirror the official Root, Trigger, and Panel anatomy, with presentation supplied at the call site using Dify UI tokens.',
},
},
},
tags: ['autodocs'],
} satisfies Meta<typeof CollapsibleRoot>
export default meta
type Story = StoryObj<typeof meta>
const rootClassName = 'w-72 rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-1 shadow-lg shadow-shadow-shadow-5'
const triggerClassName = 'h-8'
const panelClassName = 'system-sm-regular text-text-secondary'
const contentClassName = 'flex flex-col gap-2 px-2.5 pb-2 pt-1'
const iconClassName = 'i-ri-arrow-right-s-line size-4 shrink-0 text-text-tertiary transition-transform duration-100 ease-out group-data-panel-open:rotate-90 motion-reduce:transition-none'
const sectionRootClassName = 'w-90 rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-1 shadow-lg shadow-shadow-shadow-5'
const sectionTriggerClassName = cn(
triggerClassName,
'h-auto min-h-12 px-3 py-2',
)
const sectionPanelClassName = panelClassName
function TriggerIcon() {
return <span aria-hidden="true" className={iconClassName} />
}
function RecoveryKeys({
panelProps,
}: {
panelProps?: React.ComponentProps<typeof CollapsiblePanel>
}) {
return (
<>
<CollapsibleTrigger className={triggerClassName}>
Recovery keys
<TriggerIcon />
</CollapsibleTrigger>
<CollapsiblePanel className={panelClassName} {...panelProps}>
<div className={contentClassName}>
<div>alien-bean-pasta</div>
<div>wild-irish-burrito</div>
<div>horse-battery-staple</div>
</div>
</CollapsiblePanel>
</>
)
}
export const Anatomy: Story = {
args: {
defaultOpen: true,
},
render: args => (
<CollapsibleRoot {...args} className={rootClassName}>
<RecoveryKeys />
</CollapsibleRoot>
),
}
export const DefaultClosed: Story = {
render: () => (
<CollapsibleRoot className={rootClassName}>
<RecoveryKeys />
</CollapsibleRoot>
),
}
export const DefaultOpen: Story = {
render: () => (
<CollapsibleRoot defaultOpen className={rootClassName}>
<RecoveryKeys />
</CollapsibleRoot>
),
}
export const Controlled: Story = {
render: () => {
const [open, setOpen] = React.useState(true)
return (
<div className="flex flex-col items-start gap-3">
<button
type="button"
className="rounded-lg border border-divider-subtle bg-components-button-secondary-bg px-3 py-1.5 system-sm-medium text-components-button-secondary-text shadow-xs shadow-shadow-shadow-3 hover:bg-state-base-hover focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid"
onClick={() => setOpen(value => !value)}
>
{open ? 'Close panel' : 'Open panel'}
</button>
<CollapsibleRoot open={open} onOpenChange={setOpen} className={rootClassName}>
<RecoveryKeys />
</CollapsibleRoot>
</div>
)
},
}
export const Disabled: Story = {
render: () => (
<CollapsibleRoot disabled className={rootClassName}>
<RecoveryKeys />
</CollapsibleRoot>
),
}
export const KeepMounted: Story = {
render: () => (
<CollapsibleRoot className={rootClassName}>
<RecoveryKeys panelProps={{ keepMounted: true }} />
</CollapsibleRoot>
),
}
export const HiddenUntilFound: Story = {
render: () => (
<CollapsibleRoot className={rootClassName}>
<RecoveryKeys panelProps={{ hiddenUntilFound: true }} />
</CollapsibleRoot>
),
}
const settingSections = [
{
title: 'Model routing',
description: 'Fallback model enabled, retry budget set to 2 attempts.',
defaultOpen: true,
},
{
title: 'Knowledge access',
description: 'Retrieval is limited to approved workspace datasets.',
defaultOpen: false,
},
{
title: 'Observability',
description: 'Request logs and workflow traces stay available for debugging.',
defaultOpen: false,
},
] as const
export const SettingsSections: Story = {
parameters: {
layout: 'padded',
},
render: () => (
<div className={sectionRootClassName}>
{settingSections.map((section, index) => (
<CollapsibleRoot
key={section.title}
defaultOpen={section.defaultOpen}
className={cn(index > 0 && 'mt-px')}
>
<CollapsibleTrigger className={sectionTriggerClassName}>
<span className="flex min-w-0 flex-col gap-1">
<span className="truncate system-sm-medium text-text-primary">{section.title}</span>
<span className="line-clamp-2 system-xs-regular text-text-tertiary">{section.description}</span>
</span>
<TriggerIcon />
</CollapsibleTrigger>
<CollapsiblePanel className={sectionPanelClassName}>
<div className="px-3 pb-3 pt-1 system-sm-regular text-text-secondary">
{section.description}
</div>
</CollapsiblePanel>
</CollapsibleRoot>
))}
</div>
),
}

View File

@ -0,0 +1,71 @@
'use client'
import type { Collapsible as BaseCollapsibleNS } from '@base-ui/react/collapsible'
import { Collapsible as BaseCollapsible } from '@base-ui/react/collapsible'
import { cn } from '../cn'
export type CollapsibleRootProps
= Omit<BaseCollapsibleNS.Root.Props, 'className'>
& {
className?: string
}
export function CollapsibleRoot({
className,
...props
}: CollapsibleRootProps) {
return (
<BaseCollapsible.Root
className={cn('flex min-w-0 flex-col', className)}
{...props}
/>
)
}
export type CollapsibleTriggerProps
= Omit<BaseCollapsibleNS.Trigger.Props, 'className'>
& {
className?: string
}
export function CollapsibleTrigger({
className,
...props
}: CollapsibleTriggerProps) {
return (
<BaseCollapsible.Trigger
className={cn(
'group flex min-h-8 w-full touch-manipulation items-center justify-between gap-2 rounded-lg px-2.5 text-left system-sm-medium text-text-secondary outline-hidden select-none',
'hover:not-data-disabled:bg-components-panel-on-panel-item-bg-hover hover:not-data-disabled:text-text-primary',
'focus-visible:ring-2 focus-visible:ring-state-accent-solid',
'data-panel-open:text-text-primary',
'data-disabled:cursor-not-allowed data-disabled:text-text-disabled data-disabled:hover:bg-transparent',
className,
)}
{...props}
/>
)
}
export type CollapsiblePanelProps
= Omit<BaseCollapsibleNS.Panel.Props, 'className'>
& {
className?: string
}
export function CollapsiblePanel({
className,
...props
}: CollapsiblePanelProps) {
return (
<BaseCollapsible.Panel
className={cn(
'h-(--collapsible-panel-height) overflow-hidden transition-[height] duration-150 ease-out motion-reduce:transition-none',
'[&[hidden]:not([hidden=\'until-found\'])]:hidden',
'data-ending-style:h-0 data-starting-style:h-0',
className,
)}
{...props}
/>
)
}

View File

@ -1,6 +1,47 @@
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import Collapse from '../index'
import {
Collapse,
CollapseActions,
CollapseContent,
CollapseHeader,
CollapseIndicator,
CollapseTitle,
CollapseTrigger,
} from '../index'
function TestCollapse({
children,
title,
actions,
...props
}: {
title: string
children: React.ReactNode
actions?: React.ReactNode
disabled?: boolean
collapsed?: boolean
onCollapse?: (collapsed: boolean) => void
}) {
return (
<Collapse {...props}>
<CollapseHeader>
<CollapseTrigger>
<CollapseTitle>{title}</CollapseTitle>
<CollapseIndicator />
</CollapseTrigger>
{actions != null && (
<CollapseActions>
{actions}
</CollapseActions>
)}
</CollapseHeader>
<CollapseContent>
{children}
</CollapseContent>
</Collapse>
)
}
describe('Collapse', () => {
beforeEach(() => {
@ -14,12 +55,12 @@ describe('Collapse', () => {
const onCollapse = vi.fn()
render(
<Collapse
trigger={<div>Advanced</div>}
<TestCollapse
title="Advanced"
onCollapse={onCollapse}
>
<div>Collapse content</div>
</Collapse>,
</TestCollapse>,
)
expect(screen.queryByText('Collapse content')).not.toBeInTheDocument()
@ -35,13 +76,13 @@ describe('Collapse', () => {
const onCollapse = vi.fn()
render(
<Collapse
<TestCollapse
disabled
trigger={<div>Disabled section</div>}
title="Disabled section"
onCollapse={onCollapse}
>
<div>Hidden content</div>
</Collapse>,
</TestCollapse>,
)
await user.click(screen.getByText('Disabled section'))
@ -50,25 +91,19 @@ describe('Collapse', () => {
expect(onCollapse).not.toHaveBeenCalled()
})
it('should respect controlled collapse state and render function triggers', async () => {
it('should respect controlled collapse state and render actions separately', async () => {
const user = userEvent.setup()
const onCollapse = vi.fn()
render(
<Collapse
<TestCollapse
collapsed={false}
hideCollapseIcon
operations={<button type="button">Operation</button>}
trigger={collapseIcon => (
<div>
<span>Controlled section</span>
{collapseIcon}
</div>
)}
actions={<button type="button">Operation</button>}
title="Controlled section"
onCollapse={onCollapse}
>
<div>Visible content</div>
</Collapse>,
</TestCollapse>,
)
expect(screen.getByText('Visible content')).toBeInTheDocument()

View File

@ -1,36 +0,0 @@
import type { ReactNode } from 'react'
import Collapse from '.'
type FieldCollapseProps = {
title: string
children: ReactNode
collapsed?: boolean
onCollapse?: (collapsed: boolean) => void
operations?: ReactNode
}
const FieldCollapse = ({
title,
children,
collapsed,
onCollapse,
operations,
}: FieldCollapseProps) => {
return (
<div className="py-4">
<Collapse
trigger={
<div className="flex h-6 cursor-pointer items-center system-sm-semibold-uppercase text-text-secondary">{title}</div>
}
operations={operations}
collapsed={collapsed}
onCollapse={onCollapse}
>
<div className="px-4">
{children}
</div>
</Collapse>
</div>
)
}
export default FieldCollapse

View File

@ -1,69 +1,152 @@
import type { ReactNode } from 'react'
import type {
ComponentProps,
ReactNode,
} from 'react'
import { cn } from '@langgenius/dify-ui/cn'
import { useMemo, useState } from 'react'
import { ArrowDownRoundFill } from '@/app/components/base/icons/src/vender/solid/general'
import {
CollapsiblePanel,
CollapsibleRoot,
CollapsibleTrigger,
} from '@langgenius/dify-ui/collapsible'
export { default as FieldCollapse } from './field-collapse'
type CollapseProps = {
disabled?: boolean
trigger: React.JSX.Element | ((collapseIcon: React.JSX.Element | null) => React.JSX.Element)
children: React.JSX.Element
type CollapseProps = Omit<ComponentProps<typeof CollapsibleRoot>, 'open' | 'onOpenChange'> & {
collapsed?: boolean
onCollapse?: (collapsed: boolean) => void
operations?: ReactNode
hideCollapseIcon?: boolean
}
const Collapse = ({
disabled,
trigger,
children,
export function Collapse({
collapsed,
onCollapse,
operations,
hideCollapseIcon,
}: CollapseProps) => {
const [collapsedLocal, setCollapsedLocal] = useState(true)
const collapsedMerged = collapsed !== undefined ? collapsed : collapsedLocal
const collapseIcon = useMemo(() => {
if (disabled)
return null
return (
<ArrowDownRoundFill
className={cn(
'size-4 cursor-pointer text-text-quaternary group-hover/collapse:text-text-secondary',
collapsedMerged && 'rotate-270',
)}
/>
)
}, [collapsedMerged, disabled])
...props
}: CollapseProps) {
return (
<>
<div className="group/collapse flex items-center">
<div
className="ml-4 flex grow items-center"
onClick={() => {
if (!disabled) {
setCollapsedLocal(!collapsedMerged)
onCollapse?.(!collapsedMerged)
}
}}
>
{typeof trigger === 'function' ? trigger(collapseIcon) : trigger}
{!hideCollapseIcon && (
<div className="size-4 shrink-0">
{collapseIcon}
</div>
)}
</div>
{operations}
</div>
{
!collapsedMerged && children
}
</>
<CollapsibleRoot
open={collapsed === undefined ? undefined : !collapsed}
onOpenChange={open => onCollapse?.(!open)}
{...props}
/>
)
}
export default Collapse
type CollapseHeaderProps = {
children: ReactNode
}
export function CollapseHeader({
children,
}: CollapseHeaderProps) {
return (
<div className="group/collapse flex items-center">
{children}
</div>
)
}
type CollapseActionsProps = {
children: ReactNode
}
export function CollapseActions({
children,
}: CollapseActionsProps) {
return (
<div className="ml-auto shrink-0">
{children}
</div>
)
}
type CollapseTriggerProps = ComponentProps<typeof CollapsibleTrigger>
export function CollapseTrigger({
className,
...props
}: CollapseTriggerProps) {
return (
<CollapsibleTrigger
className={cn(
'group/collapse ml-4 flex h-6 min-h-0 w-auto min-w-0 shrink-0 items-center justify-start gap-0 rounded-md px-0 py-0 text-text-secondary hover:not-data-disabled:bg-transparent hover:not-data-disabled:text-text-secondary data-panel-open:text-text-secondary',
className,
)}
{...props}
/>
)
}
type CollapseTitleProps = {
children: ReactNode
className?: string
}
export function CollapseTitle({
children,
className,
}: CollapseTitleProps) {
return (
<span className={cn('min-w-0 truncate system-sm-semibold-uppercase text-text-secondary', className)}>
{children}
</span>
)
}
export function CollapseIndicator() {
return (
<span
aria-hidden="true"
className="i-custom-vender-solid-general-arrow-down-round-fill size-4 rotate-270 cursor-pointer text-text-quaternary transition-transform group-hover/collapse:text-text-secondary group-data-panel-open/collapse:rotate-0 motion-reduce:transition-none"
/>
)
}
type CollapseContentProps = ComponentProps<typeof CollapsiblePanel>
export function CollapseContent({
className,
...props
}: CollapseContentProps) {
return (
<CollapsiblePanel
className={cn(className)}
{...props}
/>
)
}
type FieldCollapseProps = {
title: string
children: ReactNode
collapsed?: boolean
onCollapse?: (collapsed: boolean) => void
actions?: ReactNode
}
export function FieldCollapse({
title,
children,
collapsed,
onCollapse,
actions,
}: FieldCollapseProps) {
return (
<div className="py-4">
<Collapse collapsed={collapsed} onCollapse={onCollapse}>
<CollapseHeader>
<CollapseTrigger>
<CollapseTitle>{title}</CollapseTitle>
<CollapseIndicator />
</CollapseTrigger>
{actions != null && (
<CollapseActions>
{actions}
</CollapseActions>
)}
</CollapseHeader>
<CollapseContent>
<div className="px-4">
{children}
</div>
</CollapseContent>
</Collapse>
</div>
)
}

View File

@ -3,10 +3,17 @@ import type {
CommonNodeType,
Node,
} from '@/app/components/workflow/types'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { Infotip } from '@/app/components/base/infotip'
import Collapse from '../collapse'
import {
Collapse,
CollapseActions,
CollapseContent,
CollapseHeader,
CollapseIndicator,
CollapseTitle,
CollapseTrigger,
} from '../collapse'
import DefaultValue from './default-value'
import ErrorHandleTypeSelector from './error-handle-type-selector'
import FailBranchCard from './fail-branch-card'
@ -17,6 +24,7 @@ import {
import { ErrorHandleTypeEnum } from './types'
type ErrorHandleProps = Pick<Node, 'id' | 'data'>
const ErrorHandle = ({
id,
data,
@ -30,64 +38,53 @@ const ErrorHandle = ({
} = useErrorHandle(id, data)
const { handleFormChange } = useDefaultValue(id)
const getHandleErrorHandleTypeChange = useCallback((data: CommonNodeType) => {
return (value: ErrorHandleTypeEnum) => {
handleErrorHandleTypeChange(value, data)
}
}, [handleErrorHandleTypeChange])
const handleTypeChange = (value: ErrorHandleTypeEnum) => {
handleErrorHandleTypeChange(value, data as CommonNodeType)
}
const getHandleFormChange = useCallback((data: CommonNodeType) => {
return (v: DefaultValueForm) => {
handleFormChange(v, data)
}
}, [handleFormChange])
const handleDefaultValueChange = (value: DefaultValueForm) => {
handleFormChange(value, data as CommonNodeType)
}
return (
<>
<div className="py-4">
<Collapse
disabled={!error_strategy}
collapsed={collapsed}
onCollapse={setCollapsed}
hideCollapseIcon
trigger={
collapseIcon => (
<div className="flex grow items-center justify-between pr-4">
<div className="flex items-center">
<div className="mr-0.5 system-sm-semibold-uppercase text-text-secondary">
{t('nodes.common.errorHandle.title', { ns: 'workflow' })}
</div>
<Infotip aria-label={t('nodes.common.errorHandle.tip', { ns: 'workflow' })}>
{t('nodes.common.errorHandle.tip', { ns: 'workflow' })}
</Infotip>
{collapseIcon}
</div>
<ErrorHandleTypeSelector
value={error_strategy || ErrorHandleTypeEnum.none}
onSelected={getHandleErrorHandleTypeChange(data)}
/>
</div>
)
}
>
<>
{
error_strategy === ErrorHandleTypeEnum.failBranch && !collapsed && (
<FailBranchCard />
)
}
{
error_strategy === ErrorHandleTypeEnum.defaultValue && !collapsed && !!default_value?.length && (
<DefaultValue
forms={default_value}
onFormChange={getHandleFormChange(data)}
/>
)
}
</>
</Collapse>
</div>
</>
<div className="py-4">
<Collapse
disabled={!error_strategy}
collapsed={collapsed}
onCollapse={setCollapsed}
>
<CollapseHeader>
<CollapseTrigger>
<CollapseTitle>
{t('nodes.common.errorHandle.title', { ns: 'workflow' })}
</CollapseTitle>
{!!error_strategy && <CollapseIndicator />}
</CollapseTrigger>
<Infotip aria-label={t('nodes.common.errorHandle.tip', { ns: 'workflow' })}>
{t('nodes.common.errorHandle.tip', { ns: 'workflow' })}
</Infotip>
<CollapseActions>
<div className="pr-4">
<ErrorHandleTypeSelector
value={error_strategy || ErrorHandleTypeEnum.none}
onSelected={handleTypeChange}
/>
</div>
</CollapseActions>
</CollapseHeader>
<CollapseContent>
{error_strategy === ErrorHandleTypeEnum.failBranch && !collapsed && (
<FailBranchCard />
)}
{error_strategy === ErrorHandleTypeEnum.defaultValue && !collapsed && !!default_value?.length && (
<DefaultValue
forms={default_value}
onFormChange={handleDefaultValueChange}
/>
)}
</CollapseContent>
</Collapse>
</div>
)
}

View File

@ -26,7 +26,7 @@ const OutputVars: FC<Props> = ({
return (
<FieldCollapse
title={title || t('nodes.common.outputVars', { ns: 'workflow' })}
operations={operations}
actions={operations}
collapsed={collapsed}
onCollapse={onCollapse}
>

View File

@ -7,7 +7,15 @@ import {
import { useTranslation } from 'react-i18next'
import { Infotip } from '@/app/components/base/infotip'
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
import Collapse from '@/app/components/workflow/nodes/_base/components/collapse'
import {
Collapse,
CollapseActions,
CollapseContent,
CollapseHeader,
CollapseIndicator,
CollapseTitle,
CollapseTrigger,
} from '@/app/components/workflow/nodes/_base/components/collapse'
import { MetadataFilteringModeEnum } from '@/app/components/workflow/nodes/knowledge-retrieval/types'
import MetadataTrigger from '../metadata-trigger'
import MetadataFilterSelector from './metadata-filter-selector'
@ -16,6 +24,7 @@ type MetadataFilterProps = {
metadataFilterMode?: MetadataFilteringModeEnum
handleMetadataFilterModeChange: (mode: MetadataFilteringModeEnum) => void
} & MetadataShape
const MetadataFilter = ({
metadataFilterMode = MetadataFilteringModeEnum.disabled,
handleMetadataFilterModeChange,
@ -39,59 +48,54 @@ const MetadataFilter = ({
disabled={metadataFilterMode === MetadataFilteringModeEnum.disabled || metadataFilterMode === MetadataFilteringModeEnum.manual}
collapsed={collapsed}
onCollapse={setCollapsed}
hideCollapseIcon
trigger={collapseIcon => (
<div className="flex grow items-center justify-between pr-4">
<div className="flex items-center">
<div className="mr-0.5 system-sm-semibold-uppercase text-text-secondary">
{t('nodes.knowledgeRetrieval.metadata.title', { ns: 'workflow' })}
</div>
<Infotip aria-label={t('nodes.knowledgeRetrieval.metadata.tip', { ns: 'workflow' })} popupClassName="w-[200px]">
{t('nodes.knowledgeRetrieval.metadata.tip', { ns: 'workflow' })}
</Infotip>
{collapseIcon}
</div>
<div className="flex items-center">
>
<CollapseHeader>
<CollapseTrigger>
<CollapseTitle>
{t('nodes.knowledgeRetrieval.metadata.title', { ns: 'workflow' })}
</CollapseTitle>
{metadataFilterMode === MetadataFilteringModeEnum.automatic && <CollapseIndicator />}
</CollapseTrigger>
<Infotip aria-label={t('nodes.knowledgeRetrieval.metadata.tip', { ns: 'workflow' })} popupClassName="w-[200px]">
{t('nodes.knowledgeRetrieval.metadata.tip', { ns: 'workflow' })}
</Infotip>
<CollapseActions>
<div className="flex items-center pr-4">
<MetadataFilterSelector
value={metadataFilterMode}
onSelect={handleMetadataFilterModeChangeWrapped}
/>
{
metadataFilterMode === MetadataFilteringModeEnum.manual && (
<div className="ml-1">
<MetadataTrigger {...restProps} />
</div>
)
}
{metadataFilterMode === MetadataFilteringModeEnum.manual && (
<div className="ml-1">
<MetadataTrigger {...restProps} />
</div>
)}
</div>
</div>
)}
>
<>
{
metadataFilterMode === MetadataFilteringModeEnum.automatic && (
<>
<div className="px-4 body-xs-regular text-text-tertiary">
{t('nodes.knowledgeRetrieval.metadata.options.automatic.desc', { ns: 'workflow' })}
</div>
<div className="mt-1 px-4">
<ModelParameterModal
popupClassName="w-[387px]!"
isInWorkflow
isAdvancedMode={true}
provider={metadataModelConfig?.provider || ''}
completionParams={metadataModelConfig?.completion_params || { temperature: 0.7 }}
modelId={metadataModelConfig?.name || ''}
setModel={handleMetadataModelChange || noop}
onCompletionParamsChange={handleMetadataCompletionParamsChange || noop}
hideDebugWithMultipleModel
debugWithMultipleModel={false}
/>
</div>
</>
)
}
</>
</CollapseActions>
</CollapseHeader>
<CollapseContent>
{metadataFilterMode === MetadataFilteringModeEnum.automatic && (
<>
<div className="px-4 body-xs-regular text-text-tertiary">
{t('nodes.knowledgeRetrieval.metadata.options.automatic.desc', { ns: 'workflow' })}
</div>
<div className="mt-1 px-4">
<ModelParameterModal
popupClassName="w-[387px]!"
isInWorkflow
isAdvancedMode={true}
provider={metadataModelConfig?.provider || ''}
completionParams={metadataModelConfig?.completion_params || { temperature: 0.7 }}
modelId={metadataModelConfig?.name || ''}
setModel={handleMetadataModelChange || noop}
onCompletionParamsChange={handleMetadataCompletionParamsChange || noop}
hideDebugWithMultipleModel
debugWithMultipleModel={false}
/>
</div>
</>
)}
</CollapseContent>
</Collapse>
)
}