mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 18:27:19 +08:00
feat: support key up and down to select variable item (#35527)
This commit is contained in:
parent
ed8d3f3e8d
commit
38fc2a6574
@ -599,6 +599,48 @@ describe('ComponentPicker (component-picker-block/index.tsx)', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('defaults to the first workflow variable and removes the full slash query when selecting by keyboard', async () => {
|
||||
const captures: Captures = { editor: null, eventEmitter: null }
|
||||
|
||||
const workflowVariableBlock = makeWorkflowVariableBlock({}, [
|
||||
makeWorkflowVarNode('node-1', 'Node 1', [
|
||||
makeWorkflowNodeVar('first_value', VarType.string),
|
||||
makeWorkflowNodeVar('second_value', VarType.string),
|
||||
]),
|
||||
])
|
||||
|
||||
render((
|
||||
<MinimalEditor
|
||||
triggerString="/"
|
||||
contextBlock={makeContextBlock()}
|
||||
workflowVariableBlock={workflowVariableBlock}
|
||||
captures={captures}
|
||||
/>
|
||||
))
|
||||
|
||||
const editor = await waitForEditor(captures)
|
||||
const dispatchSpy = vi.spyOn(editor, 'dispatchCommand')
|
||||
|
||||
await setEditorText(editor, '/e', true)
|
||||
await flushNextTick()
|
||||
|
||||
const firstItem = screen.getByText('first_value').closest('[data-selected]')
|
||||
const secondItem = screen.getByText('second_value').closest('[data-selected]')
|
||||
|
||||
expect(firstItem).toHaveAttribute('data-selected', 'true')
|
||||
expect(secondItem).toHaveAttribute('data-selected', 'false')
|
||||
|
||||
fireEvent.keyDown(document, { key: 'ArrowDown' })
|
||||
|
||||
expect(firstItem).toHaveAttribute('data-selected', 'false')
|
||||
expect(secondItem).toHaveAttribute('data-selected', 'true')
|
||||
|
||||
fireEvent.keyDown(document, { key: 'Enter' })
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledWith(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, ['node-1', 'second_value'])
|
||||
await waitFor(() => expect(readEditorText(editor)).not.toContain('/e'))
|
||||
})
|
||||
|
||||
it('skips removing the trigger when selection is null (needRemove is null) and still dispatches', async () => {
|
||||
const captures: Captures = { editor: null, eventEmitter: null }
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ import type {
|
||||
ExternalToolBlockType,
|
||||
HistoryBlockType,
|
||||
LastRunBlockType,
|
||||
MenuTextMatch,
|
||||
QueryBlockType,
|
||||
RequestURLBlockType,
|
||||
VariableBlockType,
|
||||
@ -89,14 +90,14 @@ const ComponentPicker = ({
|
||||
],
|
||||
})
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const triggerMatchRef = useRef<string | null>(null)
|
||||
const triggerMatchRef = useRef<MenuTextMatch | null>(null)
|
||||
const baseCheckForTriggerMatch = useBasicTypeaheadTriggerMatch(triggerString, {
|
||||
minLength: 0,
|
||||
maxLength: 75,
|
||||
})
|
||||
const checkForTriggerMatch = useCallback((text: string, editor: LexicalEditor) => {
|
||||
const match = baseCheckForTriggerMatch(text, editor)
|
||||
triggerMatchRef.current = match?.matchingString ?? null
|
||||
triggerMatchRef.current = match
|
||||
return match
|
||||
}, [baseCheckForTriggerMatch])
|
||||
|
||||
@ -183,7 +184,8 @@ const ComponentPicker = ({
|
||||
|
||||
const handleSelectWorkflowVariable = useCallback((variables: string[]) => {
|
||||
editor.update(() => {
|
||||
const needRemove = $splitNodeContainingQuery(checkForTriggerMatch(triggerString, editor)!)
|
||||
const currentTriggerMatch = triggerMatchRef.current ?? checkForTriggerMatch(triggerString, editor)
|
||||
const needRemove = currentTriggerMatch ? $splitNodeContainingQuery(currentTriggerMatch) : null
|
||||
if (needRemove)
|
||||
needRemove.remove()
|
||||
})
|
||||
@ -214,7 +216,7 @@ const ComponentPicker = ({
|
||||
anchorElementRef,
|
||||
{ options, selectedIndex, selectOptionAndCleanUp, setHighlightedIndex },
|
||||
) => {
|
||||
const effectiveQueryString = triggerMatchRef.current ?? queryString
|
||||
const effectiveQueryString = triggerMatchRef.current?.matchingString ?? queryString
|
||||
|
||||
if (blurHidden)
|
||||
return null
|
||||
|
||||
@ -52,6 +52,42 @@ describe('VarReferenceVars', () => {
|
||||
expect(onClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should select the first visible variable by default and support arrow navigation in slash mode', () => {
|
||||
const onChange = vi.fn()
|
||||
|
||||
render(
|
||||
<VarReferenceVars
|
||||
hideSearch
|
||||
vars={createVars([{
|
||||
title: 'Node A',
|
||||
nodeId: 'node-a',
|
||||
vars: [
|
||||
{ variable: 'first_value', type: VarType.string },
|
||||
{ variable: 'second_value', type: VarType.string },
|
||||
],
|
||||
}])}
|
||||
onChange={onChange}
|
||||
/>,
|
||||
)
|
||||
|
||||
const firstItem = screen.getByText('first_value').closest('[data-selected]')
|
||||
const secondItem = screen.getByText('second_value').closest('[data-selected]')
|
||||
|
||||
expect(firstItem).toHaveAttribute('data-selected', 'true')
|
||||
expect(secondItem).toHaveAttribute('data-selected', 'false')
|
||||
|
||||
fireEvent.keyDown(document, { key: 'ArrowDown' })
|
||||
|
||||
expect(firstItem).toHaveAttribute('data-selected', 'false')
|
||||
expect(secondItem).toHaveAttribute('data-selected', 'true')
|
||||
|
||||
fireEvent.keyDown(document, { key: 'Enter' })
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(['node-a', 'second_value'], expect.objectContaining({
|
||||
variable: 'second_value',
|
||||
}))
|
||||
})
|
||||
|
||||
it('should call onChange when a variable item is chosen', () => {
|
||||
const onChange = vi.fn()
|
||||
|
||||
@ -172,6 +208,43 @@ describe('VarReferenceVars', () => {
|
||||
expect(onChange).toHaveBeenNthCalledWith(4, ['node-special', 'asset'], expect.objectContaining({ variable: 'asset' }))
|
||||
})
|
||||
|
||||
it('should resolve selectors for special variables and file support from keyboard selection', () => {
|
||||
const onChange = vi.fn()
|
||||
|
||||
render(
|
||||
<VarReferenceVars
|
||||
hideSearch
|
||||
isSupportFileVar
|
||||
vars={createVars([
|
||||
{
|
||||
title: 'Specials',
|
||||
nodeId: 'node-special',
|
||||
vars: [
|
||||
{ variable: 'env.API_KEY', type: VarType.string },
|
||||
{ variable: 'conversation.user_name', type: VarType.string, des: 'User name' },
|
||||
{ variable: 'current', type: VarType.string },
|
||||
{ variable: 'asset', type: VarType.file },
|
||||
],
|
||||
},
|
||||
])}
|
||||
onChange={onChange}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.keyDown(document, { key: 'Enter' })
|
||||
fireEvent.keyDown(document, { key: 'ArrowDown' })
|
||||
fireEvent.keyDown(document, { key: 'Enter' })
|
||||
fireEvent.keyDown(document, { key: 'ArrowDown' })
|
||||
fireEvent.keyDown(document, { key: 'Enter' })
|
||||
fireEvent.keyDown(document, { key: 'ArrowDown' })
|
||||
fireEvent.keyDown(document, { key: 'Enter' })
|
||||
|
||||
expect(onChange).toHaveBeenNthCalledWith(1, ['env', 'API_KEY'], expect.objectContaining({ variable: 'env.API_KEY' }))
|
||||
expect(onChange).toHaveBeenNthCalledWith(2, ['conversation', 'user_name'], expect.objectContaining({ variable: 'conversation.user_name' }))
|
||||
expect(onChange).toHaveBeenNthCalledWith(3, ['node-special', 'current'], expect.objectContaining({ variable: 'current' }))
|
||||
expect(onChange).toHaveBeenNthCalledWith(4, ['node-special', 'asset'], expect.objectContaining({ variable: 'asset' }))
|
||||
})
|
||||
|
||||
it('should render object vars and select them by node path', () => {
|
||||
const onChange = vi.fn()
|
||||
|
||||
@ -251,4 +324,26 @@ describe('VarReferenceVars', () => {
|
||||
fireEvent.click(screen.getByText('asset'))
|
||||
expect(onChange).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should ignore file vars when file support is disabled during keyboard selection', () => {
|
||||
const onChange = vi.fn()
|
||||
|
||||
render(
|
||||
<VarReferenceVars
|
||||
hideSearch
|
||||
vars={createVars([
|
||||
{
|
||||
title: 'Files',
|
||||
nodeId: 'node-files',
|
||||
vars: [{ variable: 'asset', type: VarType.file }],
|
||||
},
|
||||
])}
|
||||
onChange={onChange}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.keyDown(document, { key: 'Enter' })
|
||||
|
||||
expect(onChange).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@ -12,11 +12,8 @@ import {
|
||||
import { useHover } from 'ahooks'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ChevronRight } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import { CodeAssistant, MagicEdit } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import Input from '@/app/components/base/input'
|
||||
import PickerStructurePanel from '@/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/picker'
|
||||
import { VariableIconWithColor } from '@/app/components/workflow/nodes/_base/components/variable/variable-label'
|
||||
@ -31,6 +28,42 @@ import {
|
||||
getVariableDisplayName,
|
||||
} from './var-reference-vars.helpers'
|
||||
|
||||
const VAR_SEARCH_INPUT_CLASS_NAME = 'var-search-input'
|
||||
|
||||
const resolveValueSelector = ({
|
||||
itemData,
|
||||
isFlat,
|
||||
isSupportFileVar,
|
||||
nodeId,
|
||||
objPath,
|
||||
}: {
|
||||
itemData: Var
|
||||
isFlat?: boolean
|
||||
isSupportFileVar?: boolean
|
||||
nodeId: string
|
||||
objPath: string[]
|
||||
}) => {
|
||||
const isStructureOutput = itemData.type === VarType.object && (itemData.children as StructuredOutput)?.schema?.properties
|
||||
const isFile = itemData.type === VarType.file && !isStructureOutput
|
||||
const isSys = itemData.variable.startsWith('sys.')
|
||||
const isEnv = itemData.variable.startsWith('env.')
|
||||
const isChatVar = itemData.variable.startsWith('conversation.')
|
||||
const isRagVariable = itemData.isRagVariable
|
||||
|
||||
return getValueSelector({
|
||||
itemData,
|
||||
isFlat,
|
||||
isSupportFileVar,
|
||||
isFile,
|
||||
isSys,
|
||||
isEnv,
|
||||
isChatVar,
|
||||
isRagVariable,
|
||||
nodeId,
|
||||
objPath,
|
||||
})
|
||||
}
|
||||
|
||||
type ItemProps = {
|
||||
nodeId: string
|
||||
title: string
|
||||
@ -47,6 +80,8 @@ type ItemProps = {
|
||||
zIndex?: number
|
||||
className?: string
|
||||
preferSchemaType?: boolean
|
||||
isSelected?: boolean
|
||||
onActivate?: () => void
|
||||
}
|
||||
|
||||
const Item: FC<ItemProps> = ({
|
||||
@ -64,11 +99,11 @@ const Item: FC<ItemProps> = ({
|
||||
zIndex,
|
||||
className,
|
||||
preferSchemaType,
|
||||
isSelected,
|
||||
onActivate,
|
||||
}) => {
|
||||
const isStructureOutput = itemData.type === VarType.object && (itemData.children as StructuredOutput)?.schema?.properties
|
||||
const isFile = itemData.type === VarType.file && !isStructureOutput
|
||||
const isObj = ([VarType.object, VarType.file].includes(itemData.type) && itemData.children && (itemData.children as Var[]).length > 0)
|
||||
const isSys = itemData.variable.startsWith('sys.')
|
||||
const isEnv = itemData.variable.startsWith('env.')
|
||||
const isChatVar = itemData.variable.startsWith('conversation.')
|
||||
const isRagVariable = itemData.isRagVariable
|
||||
@ -76,15 +111,21 @@ const Item: FC<ItemProps> = ({
|
||||
if (!isFlat)
|
||||
return null
|
||||
const variable = itemData.variable
|
||||
let Icon
|
||||
switch (variable) {
|
||||
case 'current':
|
||||
Icon = isInCodeGeneratorInstructionEditor ? CodeAssistant : MagicEdit
|
||||
return <Icon className="h-3.5 w-3.5 shrink-0 text-util-colors-violet-violet-600" />
|
||||
return (
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn(
|
||||
'h-3.5 w-3.5 shrink-0 text-util-colors-violet-violet-600',
|
||||
isInCodeGeneratorInstructionEditor ? 'i-custom-vender-line-general-code-assistant' : 'i-custom-vender-line-general-magic-edit',
|
||||
)}
|
||||
/>
|
||||
)
|
||||
case 'error_message':
|
||||
return <Variable02 className="h-3.5 w-3.5 shrink-0 text-util-colors-orange-dark-orange-dark-600" />
|
||||
return <span aria-hidden className="i-custom-vender-solid-development-variable-02 h-3.5 w-3.5 shrink-0 text-util-colors-orange-dark-orange-dark-600" />
|
||||
default:
|
||||
return <Variable02 className="h-3.5 w-3.5 shrink-0 text-text-accent" />
|
||||
return <span aria-hidden className="i-custom-vender-solid-development-variable-02 h-3.5 w-3.5 shrink-0 text-text-accent" />
|
||||
}
|
||||
}, [isFlat, isInCodeGeneratorInstructionEditor, itemData.variable])
|
||||
|
||||
@ -147,15 +188,10 @@ const Item: FC<ItemProps> = ({
|
||||
const handleChosen = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
e.nativeEvent.stopImmediatePropagation()
|
||||
const valueSelector = getValueSelector({
|
||||
const valueSelector = resolveValueSelector({
|
||||
itemData,
|
||||
isFlat,
|
||||
isSupportFileVar,
|
||||
isFile,
|
||||
isSys,
|
||||
isEnv,
|
||||
isChatVar,
|
||||
isRagVariable,
|
||||
nodeId,
|
||||
objPath,
|
||||
})
|
||||
@ -173,11 +209,13 @@ const Item: FC<ItemProps> = ({
|
||||
ref={itemRef}
|
||||
className={cn(
|
||||
(isObj || isStructureOutput) ? 'pr-1' : 'pr-[18px]',
|
||||
isHovering && ((isObj || isStructureOutput) ? 'bg-components-panel-on-panel-item-bg-hover' : 'bg-state-base-hover'),
|
||||
(isHovering || isSelected) && ((isObj || isStructureOutput) ? 'bg-components-panel-on-panel-item-bg-hover' : 'bg-state-base-hover'),
|
||||
'relative flex h-6 w-full cursor-pointer items-center rounded-md pl-3',
|
||||
className,
|
||||
)}
|
||||
data-selected={isSelected ? 'true' : 'false'}
|
||||
onClick={handleChosen}
|
||||
onMouseEnter={onActivate}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
@ -210,7 +248,7 @@ const Item: FC<ItemProps> = ({
|
||||
<div className="ml-1 shrink-0 text-xs font-normal text-text-tertiary capitalize">{(preferSchemaType && itemData.schemaType) ? itemData.schemaType : itemData.type}</div>
|
||||
{
|
||||
(isObj || isStructureOutput) && (
|
||||
<ChevronRight className={cn('ml-0.5 h-3 w-3 text-text-quaternary', isHovering && 'text-text-tertiary')} />
|
||||
<span aria-hidden className={cn('ml-0.5 i-custom-vender-line-arrows-chevron-right h-3 w-3 text-text-quaternary', isHovering && 'text-text-tertiary')} />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
@ -221,7 +259,7 @@ const Item: FC<ItemProps> = ({
|
||||
open={open}
|
||||
onOpenChange={noop}
|
||||
>
|
||||
<PopoverTrigger render={itemTrigger} />
|
||||
<PopoverTrigger nativeButton={false} render={itemTrigger} />
|
||||
<PopoverContent
|
||||
placement="left-start"
|
||||
sideOffset={0}
|
||||
@ -285,25 +323,122 @@ const VarReferenceVars: FC<Props> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [internalSearchValue, setInternalSearchValue] = useState('')
|
||||
const listRef = useRef<HTMLDivElement>(null)
|
||||
const searchValue = searchText ?? internalSearchValue
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
onClose?.()
|
||||
}
|
||||
}
|
||||
|
||||
const filteredVars = useMemo(() => filterReferenceVars(vars, searchValue), [vars, searchValue])
|
||||
const selectableItems = useMemo(() => {
|
||||
return filteredVars.flatMap(node => node.vars.map(item => ({
|
||||
nodeId: node.nodeId,
|
||||
isFlat: node.isFlat,
|
||||
itemData: item,
|
||||
})))
|
||||
}, [filteredVars])
|
||||
const indexedFilteredVars = useMemo(() => {
|
||||
let optionIndex = 0
|
||||
|
||||
return filteredVars.map(node => ({
|
||||
...node,
|
||||
vars: node.vars.map(variable => ({
|
||||
variable,
|
||||
optionIndex: optionIndex++,
|
||||
})),
|
||||
}))
|
||||
}, [filteredVars])
|
||||
const [selectedIndex, setSelectedIndex] = useState(-1)
|
||||
const effectiveSelectedIndex = selectableItems.length ? Math.min(Math.max(selectedIndex, 0), selectableItems.length - 1) : -1
|
||||
|
||||
useEffect(() => {
|
||||
const listElement = listRef.current
|
||||
const selectedElement = listElement?.querySelector('[data-selected="true"]') as HTMLElement | null
|
||||
if (!listElement || !selectedElement)
|
||||
return
|
||||
|
||||
const selectedTop = selectedElement.offsetTop
|
||||
const selectedBottom = selectedTop + selectedElement.offsetHeight
|
||||
const visibleTop = listElement.scrollTop
|
||||
const visibleBottom = visibleTop + listElement.clientHeight
|
||||
|
||||
if (selectedTop < visibleTop)
|
||||
listElement.scrollTop = selectedTop
|
||||
else if (selectedBottom > visibleBottom)
|
||||
listElement.scrollTop = selectedBottom - listElement.clientHeight
|
||||
}, [effectiveSelectedIndex])
|
||||
|
||||
const selectItem = useCallback((index: number) => {
|
||||
const selectedItem = selectableItems[index]
|
||||
if (!selectedItem)
|
||||
return
|
||||
|
||||
const { itemData, nodeId, isFlat } = selectedItem
|
||||
const valueSelector = resolveValueSelector({
|
||||
itemData,
|
||||
isFlat,
|
||||
isSupportFileVar,
|
||||
nodeId,
|
||||
objPath: [],
|
||||
})
|
||||
|
||||
if (valueSelector)
|
||||
onChange(valueSelector, itemData)
|
||||
}, [isSupportFileVar, onChange, selectableItems])
|
||||
|
||||
const handleKeyboardEvent = useCallback((event: Pick<KeyboardEvent, 'key' | 'preventDefault' | 'stopPropagation'>) => {
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault()
|
||||
onClose?.()
|
||||
return
|
||||
}
|
||||
|
||||
if (!selectableItems.length)
|
||||
return
|
||||
|
||||
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
setSelectedIndex(
|
||||
event.key === 'ArrowDown'
|
||||
? Math.min(effectiveSelectedIndex + 1, selectableItems.length - 1)
|
||||
: Math.max(effectiveSelectedIndex - 1, 0),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
selectItem(effectiveSelectedIndex)
|
||||
}
|
||||
}, [effectiveSelectedIndex, onClose, selectableItems.length, selectItem])
|
||||
|
||||
const handleKeyDown = useCallback((e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
handleKeyboardEvent(e)
|
||||
}, [handleKeyboardEvent])
|
||||
|
||||
useEffect(() => {
|
||||
if (!hideSearch)
|
||||
return
|
||||
|
||||
const handleDocumentKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.altKey || event.ctrlKey || event.metaKey)
|
||||
return
|
||||
if (!['ArrowDown', 'ArrowUp', 'Enter', 'Escape'].includes(event.key))
|
||||
return
|
||||
|
||||
handleKeyboardEvent(event)
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', handleDocumentKeyDown, true)
|
||||
return () => document.removeEventListener('keydown', handleDocumentKeyDown, true)
|
||||
}, [handleKeyboardEvent, hideSearch])
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
!hideSearch && (
|
||||
<>
|
||||
<div className={cn('var-search-input-wrapper mx-2 mt-2 mb-2', searchBoxClassName)} onClick={e => e.stopPropagation()}>
|
||||
<div className={cn('mx-2 mt-2 mb-2', searchBoxClassName)} onClick={e => e.stopPropagation()}>
|
||||
<Input
|
||||
className="var-search-input"
|
||||
className={VAR_SEARCH_INPUT_CLASS_NAME}
|
||||
showLeftIcon
|
||||
showClearIcon
|
||||
value={searchValue}
|
||||
@ -328,11 +463,10 @@ const VarReferenceVars: FC<Props> = ({
|
||||
|
||||
{filteredVars.length > 0
|
||||
? (
|
||||
<div className={cn('max-h-[85vh] overflow-y-auto', maxHeightClass)}>
|
||||
|
||||
<div ref={listRef} className={cn('max-h-[85vh] overflow-x-hidden overflow-y-auto', maxHeightClass)}>
|
||||
{
|
||||
filteredVars.map((item, i) => (
|
||||
<div key={i} className={cn(!item.isFlat && 'mt-3', i === 0 && item.isFlat && 'mt-2')}>
|
||||
indexedFilteredVars.map((item, i) => (
|
||||
<div key={item.nodeId} className={cn(!item.isFlat && 'mt-3', i === 0 && item.isFlat && 'mt-2')}>
|
||||
{!item.isFlat && (
|
||||
<div
|
||||
className="truncate px-3 system-xs-medium-uppercase leading-[22px] text-text-tertiary"
|
||||
@ -341,25 +475,27 @@ const VarReferenceVars: FC<Props> = ({
|
||||
{item.title}
|
||||
</div>
|
||||
)}
|
||||
{item.vars.map((v, j) => (
|
||||
{item.vars.map(({ variable, optionIndex }) => (
|
||||
<Item
|
||||
key={j}
|
||||
key={optionIndex}
|
||||
title={item.title}
|
||||
nodeId={item.nodeId}
|
||||
objPath={[]}
|
||||
itemData={v}
|
||||
itemData={variable}
|
||||
onChange={onChange}
|
||||
itemWidth={itemWidth}
|
||||
isSupportFileVar={isSupportFileVar}
|
||||
isException={v.isException}
|
||||
isException={variable.isException}
|
||||
isLoopVar={item.isLoop}
|
||||
isFlat={item.isFlat}
|
||||
isInCodeGeneratorInstructionEditor={isInCodeGeneratorInstructionEditor}
|
||||
zIndex={zIndex}
|
||||
preferSchemaType={preferSchemaType}
|
||||
isSelected={effectiveSelectedIndex === optionIndex}
|
||||
onActivate={() => setSelectedIndex(optionIndex)}
|
||||
/>
|
||||
))}
|
||||
{item.isFlat && !filteredVars[i + 1]?.isFlat && !!filteredVars.find(item => !item.isFlat) && (
|
||||
{item.isFlat && !indexedFilteredVars[i + 1]?.isFlat && !!indexedFilteredVars.find(item => !item.isFlat) && (
|
||||
<div className="relative mt-[14px] flex items-center space-x-1">
|
||||
<div className="h-0 w-3 shrink-0 border border-divider-subtle"></div>
|
||||
<div className="system-2xs-semibold-uppercase text-text-tertiary">{t('debug.lastOutput', { ns: 'workflow' })}</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user