Compare commits

..

1 Commits

Author SHA1 Message Date
4a2f90e7ec fix(device): display sso_error query param on /device page 2026-05-28 02:05:15 -07:00
9 changed files with 384 additions and 143 deletions

View File

@ -222,10 +222,6 @@ QUEUE_MONITOR_INTERVAL=30
SWAGGER_UI_ENABLED=false
SWAGGER_UI_PATH=/swagger-ui.html
OPENAPI_ENABLED=false
OPENAPI_CORS_ALLOW_ORIGINS=
OPENAPI_KNOWN_CLIENT_IDS=difyctl
OPENAPI_RATE_LIMIT_PER_TOKEN=60
ENABLE_OAUTH_BEARER=false
DSL_EXPORT_ENCRYPT_DATASET_ID=true
DATASET_MAX_SEGMENTS_PER_REQUEST=0
ENABLE_CLEAN_EMBEDDING_CACHE_TASK=false

View File

@ -3693,6 +3693,11 @@
"count": 1
}
},
"web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"web/app/components/workflow/nodes/_base/components/variable/variable-label/base/variable-icon.tsx": {
"react/static-components": {
"count": 2

View File

@ -25,6 +25,7 @@ import {
$setSelection,
BLUR_COMMAND,
FOCUS_COMMAND,
KEY_ESCAPE_COMMAND,
} from 'lexical'
import * as React from 'react'
import { GeneratorType } from '@/app/components/app/configuration/config/automatic/types'
@ -390,7 +391,7 @@ describe('ComponentPicker (component-picker-block/index.tsx)', () => {
expect(dispatchSpy).toHaveBeenCalledWith(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, '{{foo}}')
})
it('handles workflow variable selection: flat vars (current/error_message/last_run)', async () => {
it('handles workflow variable selection: flat vars (current/error_message/last_run) and closes on Escape from search input', async () => {
const captures: Captures = { editor: null, eventEmitter: null }
const workflowVariableBlock = makeWorkflowVariableBlock({}, [
@ -444,6 +445,16 @@ describe('ComponentPicker (component-picker-block/index.tsx)', () => {
await flushNextTick()
expect(dispatchSpy).toHaveBeenCalledWith(INSERT_LAST_RUN_BLOCK_COMMAND, null)
// Re-open menu and press Escape in the VarReferenceVars search input to exercise handleClose().
await setEditorText(editor, '{', true)
await flushNextTick()
const searchInput = await screen.findByPlaceholderText('workflow.common.searchVar')
await act(async () => {
fireEvent.keyDown(searchInput, { key: 'Escape' })
})
await flushNextTick()
expect(dispatchSpy).toHaveBeenCalledWith(KEY_ESCAPE_COMMAND, expect.any(KeyboardEvent))
// Re-open menu and select a flat var that is not handled by the special-case list.
// This covers the "no-op" path in the `isFlat` branch.
dispatchSpy.mockClear()
@ -589,7 +600,7 @@ describe('ComponentPicker (component-picker-block/index.tsx)', () => {
})
})
it('removes the full slash query when selecting a workflow variable', async () => {
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({}, [
@ -614,9 +625,18 @@ describe('ComponentPicker (component-picker-block/index.tsx)', () => {
await setEditorText(editor, '/e', true)
await flushNextTick()
await act(async () => {
fireEvent.click(await screen.findByText('second_value'))
})
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'))

View File

@ -9,13 +9,15 @@ vi.mock('../var-reference-vars', () => ({
default: ({
vars,
onChange,
itemWidth,
isSupportFileVar,
}: {
vars: NodeOutPutVar[]
onChange: (value: ValueSelector, item: Var) => void
itemWidth?: number
isSupportFileVar?: boolean
}) => {
mockVarReferenceVars({ vars, onChange, isSupportFileVar })
mockVarReferenceVars({ vars, onChange, itemWidth, isSupportFileVar })
return <div data-testid="var-reference-vars">{vars.length}</div>
},
}))
@ -54,6 +56,7 @@ describe('AssignedVarReferencePopup', () => {
render(
<AssignedVarReferencePopup
vars={[createOutputVar()]}
itemWidth={280}
onChange={onChange}
/>,
)
@ -62,6 +65,7 @@ describe('AssignedVarReferencePopup', () => {
expect(mockVarReferenceVars).toHaveBeenCalledWith({
vars: [createOutputVar()],
onChange,
itemWidth: 280,
isSupportFileVar: true,
})
})

View File

@ -33,11 +33,13 @@ describe('VarReferenceVars', () => {
vars: [{ variable: 'valid_name', type: VarType.string }],
}])
it('should filter vars through the search box', () => {
it('should filter vars through the search box and call onClose on escape', () => {
const onClose = vi.fn()
render(
<VarReferenceVars
vars={baseVars}
onChange={vi.fn()}
onClose={onClose}
/>,
)
@ -45,6 +47,45 @@ describe('VarReferenceVars', () => {
target: { value: 'valid' },
})
expect(screen.getByText('valid_name')).toBeInTheDocument()
fireEvent.keyDown(screen.getByPlaceholderText('workflow.common.searchVar'), { key: 'Escape' })
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', () => {
@ -76,7 +117,7 @@ describe('VarReferenceVars', () => {
/>,
)
expect(screen.getByRole('status')).toHaveTextContent('workflow.common.noVar')
expect(screen.getByText('workflow.common.noVar')).toBeInTheDocument()
fireEvent.click(screen.getByText('manage-input'))
expect(onManageInputField).toHaveBeenCalledTimes(1)
@ -167,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()
@ -246,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()
})
})

View File

@ -9,10 +9,12 @@ import VarReferenceVars from './var-reference-vars'
type Props = {
vars: NodeOutPutVar[]
onChange: (value: ValueSelector, varDetail: Var) => void
itemWidth?: number
}
const AssignedVarReferencePopup: FC<Props> = ({
vars,
onChange,
itemWidth,
}) => {
const { t } = useTranslation()
// max-h-[300px] overflow-y-auto todo: use portal to handle long list
@ -30,6 +32,7 @@ const AssignedVarReferencePopup: FC<Props> = ({
searchBoxClassName="mt-1"
vars={vars}
onChange={onChange}
itemWidth={itemWidth}
isSupportFileVar
/>
)}

View File

@ -64,6 +64,7 @@ const VarReferencePopup: FC<Props> = ({
searchBoxClassName="mt-1"
vars={vars}
onChange={onChange}
itemWidth={itemWidth}
isSupportFileVar={isSupportFileVar}
showManageInputField={showManageRagInputFields}
onManageInputField={() => setShowInputFieldPanel?.(true)}

View File

@ -1,28 +1,20 @@
'use client'
import type { FC } from 'react'
import type { StructuredOutput } from '../../../llm/types'
import type { Field } from '@/app/components/workflow/nodes/llm/types'
import type { NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types'
import { cn } from '@langgenius/dify-ui/cn'
import {
Combobox,
ComboboxClear,
ComboboxEmpty,
ComboboxGroup,
ComboboxGroupLabel,
ComboboxInput,
ComboboxInputGroup,
ComboboxItem,
ComboboxItemText,
ComboboxList,
} from '@langgenius/dify-ui/combobox'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@langgenius/dify-ui/popover'
import { useHover } from 'ahooks'
import { noop } from 'es-toolkit/function'
import * as React from 'react'
import { useCallback, useMemo, useState } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
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'
import { VarType } from '@/app/components/workflow/types'
@ -36,18 +28,9 @@ import {
getVariableDisplayName,
} from './var-reference-vars.helpers'
const VAR_SEARCH_INPUT_CLASS_NAME = 'var-search-input'
export const VAR_REFERENCE_CHILD_POPUP_CLASS_NAME = 'var-reference-vars-child-popup'
type ReferenceVarItem = {
nodeId: string
title: string
itemData: Var
optionIndex: number
isFlat?: boolean
isException?: boolean
isLoopVar?: boolean
}
const resolveValueSelector = ({
itemData,
isFlat,
@ -83,26 +66,41 @@ const resolveValueSelector = ({
}
type ItemProps = {
item: ReferenceVarItem
nodeId: string
title: string
objPath: string[]
itemData: Var
onChange: (value: ValueSelector, item: Var) => void
onHovering?: (value: boolean) => void
itemWidth?: number
isSupportFileVar?: boolean
isException?: boolean
isLoopVar?: boolean
isFlat?: boolean
isInCodeGeneratorInstructionEditor?: boolean
className?: string
preferSchemaType?: boolean
isSelected?: boolean
onActivate?: () => void
}
function Item({
item,
const Item: FC<ItemProps> = ({
nodeId,
title,
objPath,
itemData,
onChange,
onHovering,
isSupportFileVar,
isException,
isLoopVar,
isFlat,
isInCodeGeneratorInstructionEditor,
className,
preferSchemaType,
}: ItemProps) {
const {
nodeId,
title,
itemData,
isException,
isFlat,
isLoopVar,
} = item
isSelected,
onActivate,
}) => {
const isStructureOutput = itemData.type === VarType.object && (itemData.children as StructuredOutput)?.schema?.properties
const isObj = ([VarType.object, VarType.file].includes(itemData.type) && itemData.children && (itemData.children as Var[]).length > 0)
const isEnv = itemData.variable.startsWith('env.')
@ -161,16 +159,69 @@ function Item({
return objStructuredOutput
})()
const itemRef = useRef<HTMLDivElement>(null)
const [isItemHovering, setIsItemHovering] = useState(false)
useHover(itemRef, {
onChange: (hovering) => {
if (hovering) {
setIsItemHovering(true)
}
else {
if (isObj || isStructureOutput) {
setTimeout(() => {
setIsItemHovering(false)
}, 100)
}
else {
setIsItemHovering(false)
}
}
},
})
const [isChildrenHovering, setIsChildrenHovering] = useState(false)
const isHovering = isItemHovering || isChildrenHovering
const open = (isObj || isStructureOutput) && isHovering
useEffect(() => {
onHovering?.(isHovering)
}, [isHovering, onHovering])
const handleChosen = (e: React.MouseEvent) => {
e.stopPropagation()
e.nativeEvent.stopImmediatePropagation()
const valueSelector = resolveValueSelector({
itemData,
isFlat,
isSupportFileVar,
nodeId,
objPath,
})
if (valueSelector)
onChange(valueSelector, itemData)
}
const variableCategory = useMemo(
() => getVariableCategory({ isEnv, isChatVar, isLoopVar, isRagVariable }),
[isEnv, isChatVar, isLoopVar, isRagVariable],
)
const itemTrigger = (
<ComboboxItem
value={item}
<div
ref={itemRef}
className={cn(
(isObj || isStructureOutput) ? 'pr-1' : 'pr-[18px]',
(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 outline-hidden focus:outline-hidden focus-visible:outline-hidden',
className,
)}
data-selected={isSelected ? 'true' : 'false'}
onClick={handleChosen}
onMouseEnter={onActivate}
onMouseDown={(e) => {
e.preventDefault()
e.stopPropagation()
e.nativeEvent.stopImmediatePropagation()
}}
>
<ComboboxItemText className="flex items-center gap-1 px-0">
<div className="flex w-0 grow items-center">
{!isFlat && (
<VariableIconWithColor
variables={itemData.variable.split('.')}
@ -181,33 +232,33 @@ function Item({
{isFlat && flatVarIcon}
{!isEnv && !isChatVar && !isRagVariable && (
<span title={itemData.variable} className="min-w-0 grow truncate">{varName}</span>
<div title={itemData.variable} className="ml-1 w-0 grow truncate system-sm-medium text-text-secondary">{varName}</div>
)}
{isEnv && (
<span title={itemData.variable} className="min-w-0 grow truncate">{itemData.variable.replace('env.', '')}</span>
<div title={itemData.variable} className="ml-1 w-0 grow truncate system-sm-medium text-text-secondary">{itemData.variable.replace('env.', '')}</div>
)}
{isChatVar && (
<span title={itemData.des} className="min-w-0 grow truncate">{itemData.variable.replace('conversation.', '')}</span>
<div title={itemData.des} className="ml-1 w-0 grow truncate system-sm-medium text-text-secondary">{itemData.variable.replace('conversation.', '')}</div>
)}
{isRagVariable && (
<span title={itemData.des} className="min-w-0 grow truncate">{itemData.variable.split('.').slice(-1)[0]}</span>
<div title={itemData.des} className="ml-1 w-0 grow truncate system-sm-medium text-text-secondary">{itemData.variable.split('.').slice(-1)[0]}</div>
)}
</ComboboxItemText>
<span className="text-xs font-normal text-text-tertiary capitalize">{(preferSchemaType && itemData.schemaType) ? itemData.schemaType : itemData.type}</span>
</div>
<div className="ml-1 shrink-0 text-xs font-normal text-text-tertiary capitalize">{(preferSchemaType && itemData.schemaType) ? itemData.schemaType : itemData.type}</div>
{
(isObj || isStructureOutput) && (
<span aria-hidden className="i-custom-vender-line-arrows-chevron-right size-3 text-text-quaternary" />
<span aria-hidden className={cn('ml-0.5 i-custom-vender-line-arrows-chevron-right size-3 text-text-quaternary', isHovering && 'text-text-tertiary')} />
)
}
</ComboboxItem>
</div>
)
if (!isObj && !isStructureOutput)
return itemTrigger
return (
<Popover>
<PopoverTrigger nativeButton={false} openOnHover render={itemTrigger} />
<Popover
open={open}
onOpenChange={noop}
>
<PopoverTrigger nativeButton={false} render={itemTrigger} />
<PopoverContent
placement="left-start"
sideOffset={0}
@ -217,6 +268,7 @@ function Item({
<PickerStructurePanel
root={{ nodeId, nodeName: title, attrName: itemData.variable, attrAlias: itemData.schemaType }}
payload={structuredOutput!}
onHovering={setIsChildrenHovering}
onSelect={(valueSelector) => {
onChange(valueSelector, itemData)
}}
@ -227,20 +279,6 @@ function Item({
)
}
function getReferenceVarLabel(item: ReferenceVarItem) {
return getVariableDisplayName(item.itemData.variable, !!item.isFlat)
}
function getReferenceVarValue(item: ReferenceVarItem) {
return `${item.nodeId}:${item.itemData.variable}:${item.optionIndex}`
}
function isSameReferenceVar(item: ReferenceVarItem, value: ReferenceVarItem) {
return item.nodeId === value.nodeId
&& item.itemData.variable === value.itemData.variable
&& item.optionIndex === value.optionIndex
}
type Props = {
hideSearch?: boolean
searchText?: string
@ -248,6 +286,7 @@ type Props = {
vars: NodeOutPutVar[]
isSupportFileVar?: boolean
onChange: (value: ValueSelector, item: Var) => void
itemWidth?: number
maxHeightClass?: string
onClose?: () => void
onBlur?: () => void
@ -257,44 +296,68 @@ type Props = {
autoFocus?: boolean
preferSchemaType?: boolean
}
function VarReferenceVars({
const VarReferenceVars: FC<Props> = ({
hideSearch,
searchText,
searchBoxClassName,
vars,
isSupportFileVar,
onChange,
itemWidth,
maxHeightClass,
onClose,
onBlur,
isInCodeGeneratorInstructionEditor,
showManageInputField,
onManageInputField,
autoFocus = true,
preferSchemaType,
}: Props) {
}) => {
const { t } = useTranslation()
const [internalSearchValue, setInternalSearchValue] = useState('')
const listRef = useRef<HTMLDivElement>(null)
const searchValue = searchText ?? internalSearchValue
const filteredVars = useMemo(() => filterReferenceVars(vars, searchValue), [vars, searchValue])
const groupedItems = useMemo(() => {
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): ReferenceVarItem => ({
nodeId: node.nodeId,
title: node.title,
itemData: variable,
isFlat: node.isFlat,
isException: variable.isException,
isLoopVar: node.isLoop,
vars: node.vars.map(variable => ({
variable,
optionIndex: optionIndex++,
})),
}))
}, [filteredVars])
const selectableItems = useMemo(() => groupedItems.flatMap(node => node.vars), [groupedItems])
const [selectedIndex, setSelectedIndex] = useState(-1)
const effectiveSelectedIndex = selectableItems.length ? Math.min(Math.max(selectedIndex, 0), selectableItems.length - 1) : -1
const selectItem = useCallback((selectedItem?: ReferenceVarItem) => {
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
@ -309,97 +372,141 @@ function VarReferenceVars({
if (valueSelector)
onChange(valueSelector, itemData)
}, [isSupportFileVar, onChange])
}, [isSupportFileVar, onChange, selectableItems])
const handleValueChange = useCallback((item: ReferenceVarItem | null) => {
if (!item)
const handleKeyboardEvent = useCallback((event: Pick<KeyboardEvent, 'key' | 'preventDefault' | 'stopPropagation'>) => {
if (event.key === 'Escape') {
event.preventDefault()
onClose?.()
return
}
if (!selectableItems.length)
return
selectItem(item)
}, [selectItem])
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
}
const handleInputValueChange = useCallback((value: string) => {
if (searchText === undefined)
setInternalSearchValue(value)
}, [searchText])
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 (
<Combobox<ReferenceVarItem>
inline
open
value={null}
items={selectableItems}
inputValue={searchValue}
onInputValueChange={handleInputValueChange}
onValueChange={handleValueChange}
filter={null}
itemToStringLabel={getReferenceVarLabel}
itemToStringValue={getReferenceVarValue}
isItemEqualToValue={isSameReferenceVar}
>
<>
{
!hideSearch && (
<div className={cn('m-2', searchBoxClassName)}>
<ComboboxInputGroup>
<ComboboxInput
aria-label={t('common.searchVar', { ns: 'workflow' }) || ''}
<>
<div className={cn('m-2', searchBoxClassName)} onClick={e => e.stopPropagation()}>
<Input
className={VAR_SEARCH_INPUT_CLASS_NAME}
showLeftIcon
showClearIcon
value={searchValue}
placeholder={t('common.searchVar', { ns: 'workflow' }) || ''}
onChange={e => setInternalSearchValue(e.target.value)}
onKeyDown={handleKeyDown}
onClear={() => setInternalSearchValue('')}
onBlur={onBlur}
autoFocus={autoFocus}
/>
{searchValue && (
<ComboboxClear
aria-label={t('operation.clear', { ns: 'common' })}
/>
)}
</ComboboxInputGroup>
</div>
</div>
<div
className="relative left-[-4px] h-[0.5px] bg-black/5"
style={{
width: 'calc(100% + 8px)',
}}
>
</div>
</>
)
}
{filteredVars.length > 0
? (
<ComboboxList className={maxHeightClass}>
<div ref={listRef} className={cn('max-h-[85vh] overflow-x-hidden overflow-y-auto', maxHeightClass)}>
{
groupedItems.map((group, i) => (
<ComboboxGroup key={group.nodeId} items={group.vars}>
{!group.isFlat && (
<ComboboxGroupLabel
title={group.title}
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"
title={item.title}
>
{group.title}
</ComboboxGroupLabel>
{item.title}
</div>
)}
{group.vars.map(item => (
{item.vars.map(({ variable, optionIndex }) => (
<Item
key={item.optionIndex}
item={item}
key={optionIndex}
title={item.title}
nodeId={item.nodeId}
objPath={[]}
itemData={variable}
onChange={onChange}
itemWidth={itemWidth}
isSupportFileVar={isSupportFileVar}
isException={variable.isException}
isLoopVar={item.isLoop}
isFlat={item.isFlat}
isInCodeGeneratorInstructionEditor={isInCodeGeneratorInstructionEditor}
preferSchemaType={preferSchemaType}
isSelected={effectiveSelectedIndex === optionIndex}
onActivate={() => setSelectedIndex(optionIndex)}
/>
))}
{group.isFlat && !groupedItems[i + 1]?.isFlat && !!groupedItems.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>
<div className="h-0 shrink-0 grow border border-divider-subtle"></div>
</div>
)}
</ComboboxGroup>
</div>
))
}
</ComboboxList>
</div>
)
: <ComboboxEmpty>{t('common.noVar', { ns: 'workflow' })}</ComboboxEmpty>}
: <div className="mt-2 pl-3 text-xs leading-[18px] font-medium text-gray-500 uppercase">{t('common.noVar', { ns: 'workflow' })}</div>}
{
showManageInputField && onManageInputField && (
showManageInputField && (
<ManageInputField
onManage={onManageInputField}
onManage={onManageInputField || noop}
/>
)
}
</Combobox>
</>
)
}

View File

@ -33,6 +33,7 @@ export default function DevicePage() {
const pathname = usePathname()
const urlUserCode = (searchParams.get('user_code') || '').trim().toUpperCase()
const ssoVerified = searchParams.get('sso_verified') === '1'
const ssoError = searchParams.get('sso_error') || ''
const [typed, setTyped] = useState('')
const [view, setView] = useState<View>({ kind: 'code_entry' })
@ -81,7 +82,11 @@ export default function DevicePage() {
return
}
let consumed = false
if (ssoVerified) {
if (ssoError) {
setErrMsg(ssoError) // eslint-disable-line react/set-state-in-effect
consumed = true
}
else if (ssoVerified) {
setView({ kind: 'authorize_sso' }) // eslint-disable-line react/set-state-in-effect
consumed = true
}
@ -92,9 +97,9 @@ export default function DevicePage() {
setView({ kind: 'chooser', userCode: urlUserCode }) // eslint-disable-line react/set-state-in-effect
consumed = true
}
if (consumed && (urlUserCode || ssoVerified))
if (consumed && (urlUserCode || ssoVerified || ssoError))
router.replace(pathname)
}, [urlUserCode, ssoVerified, account, view, router, pathname])
}, [urlUserCode, ssoVerified, ssoError, account, view, router, pathname])
const onContinue = async () => {
if (!isValidUserCode(typed))