refactor(web): remove workflow node useBoolean usage (#39729)

This commit is contained in:
yyh 2026-07-29 11:07:30 +08:00 committed by GitHub
parent e604fad630
commit 8946d7fbf4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 149 additions and 403 deletions

View File

@ -5022,11 +5022,6 @@
"count": 3
}
},
"web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/__tests__/list.spec.tsx": {
"no-unused-vars": {
"count": 1
}
},
"web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/item.tsx": {
"jsx_a11y/click-events-have-key-events": {
"count": 2

View File

@ -2,28 +2,8 @@ import { fireEvent, render, screen } from '@testing-library/react'
import Field from '../field'
describe('Field', () => {
it('should render subtitle styling, tooltip, operations, warning dot and required marker', () => {
const { container } = render(
<Field
title="Knowledge"
tooltip="tooltip text"
operations={<button type="button">operation</button>}
required
warningDot
isSubTitle
/>,
)
expect(screen.getByText('Knowledge')).toBeInTheDocument()
expect(screen.getByLabelText('tooltip text')).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'operation' })).toBeInTheDocument()
expect(screen.getByText('*')).toBeInTheDocument()
expect(container.querySelector('.system-xs-medium-uppercase')).not.toBeNull()
expect(container.querySelector('.bg-text-warning-secondary')).not.toBeNull()
})
it('should toggle folded children when supportFold is enabled', () => {
const { container } = render(
render(
<Field title="Foldable" supportFold>
<div>folded content</div>
</Field>,
@ -33,20 +13,8 @@ describe('Field', () => {
fireEvent.click(screen.getByText('Foldable').closest('.cursor-pointer')!)
expect(screen.getByText('folded content')).toBeInTheDocument()
expect(container.querySelector('svg')).toHaveStyle({ transform: 'rotate(0deg)' })
fireEvent.click(screen.getByText('Foldable').closest('.cursor-pointer')!)
expect(screen.queryByText('folded content')).not.toBeInTheDocument()
})
it('should render inline children without folding support', () => {
const { container } = render(
<Field title="Inline" inline>
<div>always visible</div>
</Field>,
)
expect(screen.getByText('always visible')).toBeInTheDocument()
expect(container.firstChild).toHaveClass('flex')
})
})

View File

@ -3,9 +3,8 @@ import type { FC } from 'react'
import type { CodeLanguage } from '../../code/types'
import type { GenRes } from '@/service/debug'
import { cn } from '@langgenius/dify-ui/cn'
import { useBoolean } from 'ahooks'
import * as React from 'react'
import { useCallback } from 'react'
import { useCallback, useState } from 'react'
import { GetCodeGeneratorResModal } from '@/app/components/app/configuration/config/code-generator/get-code-generator-res'
import { ActionButton } from '@/app/components/base/action-button'
import { Generator } from '@/app/components/base/icons/src/vender/other'
@ -27,20 +26,19 @@ const CodeGenerateBtn: FC<Props> = ({
codeLanguages,
onGenerated,
}) => {
const [showAutomatic, { setTrue: showAutomaticTrue, setFalse: showAutomaticFalse }] =
useBoolean(false)
const [showAutomatic, setShowAutomatic] = useState(false)
const handleAutomaticRes = useCallback(
(res: GenRes) => {
onGenerated?.(res.modified)
showAutomaticFalse()
setShowAutomatic(false)
},
[onGenerated, showAutomaticFalse],
[onGenerated],
)
const configsMap = useHooksStore((s) => s.configsMap)
return (
<div className={cn(className)}>
<ActionButton className="hover:bg-[#155EFF]/8" onClick={showAutomaticTrue}>
<ActionButton className="hover:bg-[#155EFF]/8" onClick={() => setShowAutomatic(true)}>
<Generator className="size-4 text-primary-600" />
</ActionButton>
{showAutomatic && (
@ -48,7 +46,7 @@ const CodeGenerateBtn: FC<Props> = ({
mode={AppModeEnum.CHAT}
isShow={showAutomatic}
codeLanguages={codeLanguages}
onClose={showAutomaticFalse}
onClose={() => setShowAutomatic(false)}
onFinished={handleAutomaticRes}
flowId={configsMap?.flowId || ''}
nodeId={nodeId}

View File

@ -3,7 +3,6 @@ import type { FC } from 'react'
import type { Props as EditorProps } from '.'
import type { NodeOutPutVar, Variable } from '@/app/components/workflow/types'
import { cn } from '@langgenius/dify-ui/cn'
import { useBoolean } from 'ahooks'
import * as React from 'react'
import { useEffect, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
@ -29,7 +28,7 @@ const CodeEditor: FC<Props> = ({ availableVars, varList, onAddVar, ...editorProp
const monacoRef = useRef(null)
const popupRef = useRef<HTMLDivElement>(null)
const [isShowVarPicker, { setTrue: showVarPicker, setFalse: hideVarPicker }] = useBoolean(false)
const [isShowVarPicker, setIsShowVarPicker] = useState(false)
const [popupPosition, setPopupPosition] = useState({ x: 0, y: 0 })
@ -48,9 +47,9 @@ const CodeEditor: FC<Props> = ({ availableVars, varList, onAddVar, ...editorProp
const popupY = editorRect.top + cursorCoords.top + 20 // Adjust the vertical position as needed
setPopupPosition({ x: popupX, y: popupY })
showVarPicker()
setIsShowVarPicker(true)
} else {
hideVarPicker()
setIsShowVarPicker(false)
}
}
@ -137,7 +136,7 @@ const CodeEditor: FC<Props> = ({ availableVars, varList, onAddVar, ...editorProp
},
])
hideVarPicker()
setIsShowVarPicker(false)
}
return (

View File

@ -2,8 +2,8 @@
import type { FC, ReactNode } from 'react'
import { cn } from '@langgenius/dify-ui/cn'
import { RiArrowDownSLine } from '@remixicon/react'
import { useBoolean } from 'ahooks'
import * as React from 'react'
import { useState } from 'react'
import { Infotip } from '@/app/components/base/infotip'
type Props = Readonly<{
@ -40,7 +40,7 @@ const Field: FC<Props> = ({
required,
warningDot,
}) => {
const [fold, { toggle: toggleFold }] = useBoolean(true)
const [fold, setFold] = useState(true)
const tooltipLabel = tooltip
? getTextFromNode(tooltip) || getTextFromNode(title) || 'Help'
: undefined
@ -48,7 +48,7 @@ const Field: FC<Props> = ({
return (
<div className={cn(className, inline && 'flex w-full items-center justify-between')}>
<div
onClick={() => supportFold && toggleFold()}
onClick={() => supportFold && setFold((isFolded) => !isFolded)}
className={cn('flex items-center justify-between', supportFold && 'cursor-pointer')}
>
<div className="flex h-6 items-center">

View File

@ -1,8 +1,9 @@
'use client'
import type { FC } from 'react'
import { cn } from '@langgenius/dify-ui/cn'
import { useBoolean, useClickAway } from 'ahooks'
import { useClickAway } from 'ahooks'
import * as React from 'react'
import { useState } from 'react'
import { ChevronSelectorVertical } from '@/app/components/base/icons/src/vender/line/arrows'
import { Check } from '@/app/components/base/icons/src/vender/line/general'
@ -49,10 +50,10 @@ const TypeSelector: FC<Props> = ({
const item = allOptions
? allOptions.find((item) => item.value === value)
: list.find((item) => item.value === value)
const [showOption, { setFalse: setHide, toggle: toggleShow }] = useBoolean(false)
const [showOption, setShowOption] = useState(false)
const ref = React.useRef(null)
useClickAway(() => {
setHide()
setShowOption(false)
}, ref)
return (
<div
@ -60,12 +61,15 @@ const TypeSelector: FC<Props> = ({
ref={ref}
>
{trigger ? (
<div onClick={toggleShow} className={cn(!readonly && 'cursor-pointer')}>
<div
onClick={() => setShowOption((isShown) => !isShown)}
className={cn(!readonly && 'cursor-pointer')}
>
{trigger}
</div>
) : (
<div
onClick={toggleShow}
onClick={() => setShowOption((isShown) => !isShown)}
className={cn(
showOption && 'bg-state-base-hover',
'flex h-5 cursor-pointer items-center rounded-md pr-0.5 pl-1 text-xs font-semibold text-text-secondary hover:bg-state-base-hover',
@ -96,7 +100,7 @@ const TypeSelector: FC<Props> = ({
<div
key={item.value}
onClick={() => {
setHide()
setShowOption(false)
onChange(item.value)
}}
className={cn(

View File

@ -3,7 +3,6 @@
import type { FC, ReactNode } from 'react'
import { cn } from '@langgenius/dify-ui/cn'
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
import { useBoolean } from 'ahooks'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Badge from '@/app/components/base/badge'
@ -29,8 +28,7 @@ export const SwitchPluginVersion: FC<SwitchPluginVersionProps> = (props) => {
const [pluginId] = uniqueIdentifier?.split(':') || ['']
const [isShow, setIsShow] = useState(false)
const [isShowUpdateModal, { setTrue: showUpdateModal, setFalse: hideUpdateModal }] =
useBoolean(false)
const [isShowUpdateModal, setIsShowUpdateModal] = useState(false)
const [target, setTarget] = useState<{
version: string
pluginUniqueIden: string
@ -43,10 +41,10 @@ export const SwitchPluginVersion: FC<SwitchPluginVersionProps> = (props) => {
const pluginDetail = pluginDetails.data?.plugins.at(0)
const handleUpdatedFromMarketplace = useCallback(() => {
hideUpdateModal()
setIsShowUpdateModal(false)
pluginDetails.refetch()
onChange?.(target!.version)
}, [hideUpdateModal, onChange, pluginDetails, target])
}, [onChange, pluginDetails, target])
const { getIconUrl } = useGetIcon()
const icon = pluginDetail?.declaration.icon
? getIconUrl(pluginDetail.declaration.icon)
@ -77,7 +75,7 @@ export const SwitchPluginVersion: FC<SwitchPluginVersionProps> = (props) => {
>
{isShowUpdateModal && pluginDetail && (
<PluginMutationModel
onCancel={hideUpdateModal}
onCancel={() => setIsShowUpdateModal(false)}
plugin={pluginManifestToCardPluginProps({
...pluginDetail.declaration,
icon: icon!,
@ -123,7 +121,7 @@ export const SwitchPluginVersion: FC<SwitchPluginVersionProps> = (props) => {
pluginUniqueIden: state.unique_identifier,
version: state.version,
})
showUpdateModal()
setIsShowUpdateModal(true)
}}
trigger={
<Badge

View File

@ -3,8 +3,8 @@ import type { FC } from 'react'
import type { Field as FieldType } from '../../../../../llm/types'
import { cn } from '@langgenius/dify-ui/cn'
import { RiArrowDropDownLine } from '@remixicon/react'
import { useBoolean } from 'ahooks'
import * as React from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Type } from '../../../../../llm/types'
import { getFieldType } from '../../../../../llm/utils'
@ -23,7 +23,7 @@ const Field: FC<Props> = ({ name, payload, depth = 1, required, rootClassName })
const isRoot = depth === 1
const hasChildren = payload.type === Type.object && payload.properties
const hasEnum = payload.enum && payload.enum.length > 0
const [fold, { toggle: toggleFold }] = useBoolean(false)
const [fold, setFold] = useState(false)
return (
<div>
<div className={cn('flex pr-2')}>
@ -36,7 +36,7 @@ const Field: FC<Props> = ({ name, payload, depth = 1, required, rootClassName })
'absolute top-[50%] left-[-18px] h-4 w-4 translate-y-[-50%] cursor-pointer bg-components-panel-bg text-text-tertiary',
fold && 'rotate-270 text-text-accent',
)}
onClick={toggleFold}
onClick={() => setFold((isFolded) => !isFolded)}
/>
)}
<div

View File

@ -1,6 +1,6 @@
import type { CodeNodeType, OutputVar } from '../../code/types'
import type { ValueSelector } from '@/app/components/workflow/types'
import { useBoolean, useDebounceFn } from 'ahooks'
import { useDebounceFn } from 'ahooks'
import { produce } from 'immer'
import { useCallback, useRef, useState } from 'react'
import { ErrorHandleTypeEnum } from '@/app/components/workflow/nodes/_base/components/error-handle/types'
@ -116,10 +116,7 @@ function useOutputVarList<T>({
onOutputKeyOrdersChange([...outputKeyOrders, newKey])
}, [generateNewKey, inputs, setInputs, onOutputKeyOrdersChange, outputKeyOrders, varKey])
const [
isShowRemoveVarConfirm,
{ setTrue: showRemoveVarConfirm, setFalse: hideRemoveVarConfirm },
] = useBoolean(false)
const [isShowRemoveVarConfirm, setIsShowRemoveVarConfirm] = useState(false)
const [removedVar, setRemovedVar] = useState<ValueSelector>([])
const removeVarInNode = useCallback(() => {
const varId = nodesWithInspectVars
@ -129,21 +126,14 @@ function useOutputVarList<T>({
})?.id
if (varId) deleteInspectVar(id, varId)
removeUsedVarInNodes(removedVar)
hideRemoveVarConfirm()
}, [
deleteInspectVar,
hideRemoveVarConfirm,
id,
nodesWithInspectVars,
removeUsedVarInNodes,
removedVar,
])
setIsShowRemoveVarConfirm(false)
}, [deleteInspectVar, id, nodesWithInspectVars, removeUsedVarInNodes, removedVar])
const handleRemoveVariable = useCallback(
(index: number) => {
const key = outputKeyOrders[index]!
if (isVarUsedInNodes([id, key])) {
showRemoveVarConfirm()
setIsShowRemoveVarConfirm(true)
setRemovedVar([id, key])
return
}
@ -180,7 +170,6 @@ function useOutputVarList<T>({
onOutputKeyOrdersChange,
nodesWithInspectVars,
deleteInspectVar,
showRemoveVarConfirm,
varKey,
],
)
@ -190,7 +179,7 @@ function useOutputVarList<T>({
handleAddVariable,
handleRemoveVariable,
isShowRemoveVarConfirm,
hideRemoveVarConfirm,
hideRemoveVarConfirm: () => setIsShowRemoveVarConfirm(false),
onRemoveVarConfirm: removeVarInNode,
}
}

View File

@ -1,5 +1,4 @@
import type { KeyValue } from '../types'
import { useBoolean } from 'ahooks'
import { uniqueId } from 'es-toolkit/compat'
import { useCallback, useEffect, useState } from 'react'
@ -63,14 +62,14 @@ const useKeyValueList = (value: string, onChange: (value: string) => void, noFil
])
}, [list, setList])
const [isKeyValueEdit, { toggle: toggleIsKeyValueEdit }] = useBoolean(true)
const [isKeyValueEdit, setIsKeyValueEdit] = useState(true)
return {
list: list.length === 0 ? [{ id: uniqueId(UNIQUE_ID_PREFIX), key: '', value: '' }] : list, // no item can not add new item
setList,
addItem,
isKeyValueEdit,
toggleIsKeyValueEdit,
toggleIsKeyValueEdit: () => setIsKeyValueEdit((isEditing) => !isEditing),
}
}

View File

@ -1,6 +1,5 @@
import type { Var } from '../../types'
import type { Authorization, Body, HttpNodeType, Method, Timeout } from './types'
import { useBoolean } from 'ahooks'
import { produce } from 'immer'
import { useCallback, useEffect, useState } from 'react'
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
@ -113,8 +112,7 @@ const useConfig = (id: string, payload: HttpNodeType) => {
)
// authorization
const [isShowAuthorization, { setTrue: showAuthorization, setFalse: hideAuthorization }] =
useBoolean(false)
const [isShowAuthorization, setIsShowAuthorization] = useState(false)
const setAuthorization = useCallback(
(authorization: Authorization) => {
@ -141,7 +139,7 @@ const useConfig = (id: string, payload: HttpNodeType) => {
}, [])
// curl import panel
const [isShowCurlPanel, { setTrue: showCurlPanel, setFalse: hideCurlPanel }] = useBoolean(false)
const [isShowCurlPanel, setIsShowCurlPanel] = useState(false)
const handleCurlImport = useCallback(
(newNode: HttpNodeType) => {
@ -194,14 +192,14 @@ const useConfig = (id: string, payload: HttpNodeType) => {
handleSSLVerifyChange,
// authorization
isShowAuthorization,
showAuthorization,
hideAuthorization,
showAuthorization: () => setIsShowAuthorization(true),
hideAuthorization: () => setIsShowAuthorization(false),
setAuthorization,
setTimeout,
// curl import
isShowCurlPanel,
showCurlPanel,
hideCurlPanel,
showCurlPanel: () => setIsShowCurlPanel(true),
hideCurlPanel: () => setIsShowCurlPanel(false),
handleCurlImport,
}
}

View File

@ -11,10 +11,9 @@ import {
RiExpandDiagonalLine,
RiEyeLine,
} from '@remixicon/react'
import { useBoolean } from 'ahooks'
import copy from 'copy-to-clipboard'
import * as React from 'react'
import { useCallback } from 'react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import Divider from '@/app/components/base/divider'
@ -70,10 +69,10 @@ const Panel: FC<NodePanelProps<HumanInputNodeType>> = ({ id, data }) => {
},
})
const [isExpandFormContent, { toggle: toggleExpandFormContent }] = useBoolean(false)
const [isExpandFormContent, setIsExpandFormContent] = useState(false)
const nodePanelWidth = useStore((state) => state.nodePanelWidth)
const [isPreview, { toggle: togglePreview, setFalse: hidePreview }] = useBoolean(false)
const [isPreview, setIsPreview] = useState(false)
const onAddUseAction = useCallback(() => {
const index = inputs.user_actions.length + 1
@ -131,7 +130,7 @@ const Panel: FC<NodePanelProps<HumanInputNodeType>> = ({ id, data }) => {
'flex items-center space-x-1 px-2',
isPreview && 'bg-state-accent-active text-text-accent',
)}
onClick={togglePreview}
onClick={() => setIsPreview((isPreview) => !isPreview)}
>
<RiEyeLine className="size-3.5" />
<div className="system-xs-medium">
@ -160,7 +159,7 @@ const Panel: FC<NodePanelProps<HumanInputNodeType>> = ({ id, data }) => {
'flex size-6 cursor-pointer items-center justify-center rounded-md border-none bg-transparent p-0 text-text-secondary hover:bg-components-button-ghost-bg-hover',
isExpandFormContent && 'bg-state-accent-active text-text-accent',
)}
onClick={toggleExpandFormContent}
onClick={() => setIsExpandFormContent((isExpanded) => !isExpanded)}
>
{isExpandFormContent ? (
<RiCollapseDiagonalLine className="size-4" aria-hidden />
@ -263,7 +262,7 @@ const Panel: FC<NodePanelProps<HumanInputNodeType>> = ({ id, data }) => {
content={inputs.form_content}
formInputs={inputs.inputs}
userActions={inputs.user_actions}
onClose={hidePreview}
onClose={() => setIsPreview(false)}
/>
)}
</div>

View File

@ -1,9 +1,8 @@
'use client'
import type { FC } from 'react'
import type { DataSet } from '@/models/datasets'
import { useBoolean } from 'ahooks'
import * as React from 'react'
import { useCallback } from 'react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import SelectDataset from '@/app/components/app/configuration/dataset-config/select-dataset'
@ -15,14 +14,14 @@ type Props = Readonly<{
const AddDataset: FC<Props> = ({ selectedIds, modal, onChange }) => {
const { t } = useTranslation()
const [isShowModal, { setTrue: showModal, setFalse: hideModal }] = useBoolean(false)
const [isShowModal, setIsShowModal] = useState(false)
const handleSelect = useCallback(
(datasets: DataSet[]) => {
onChange(datasets)
hideModal()
setIsShowModal(false)
},
[onChange, hideModal],
[onChange],
)
return (
<div>
@ -30,14 +29,14 @@ const AddDataset: FC<Props> = ({ selectedIds, modal, onChange }) => {
type="button"
aria-label={`${t(($) => $['operation.add'], { ns: 'common' })} ${t(($) => $['nodes.knowledgeRetrieval.knowledge'], { ns: 'workflow' })}`}
className="cursor-pointer rounded-md border-none bg-transparent p-1 outline-hidden select-none hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid"
onClick={showModal}
onClick={() => setIsShowModal(true)}
>
<span aria-hidden="true" className="i-ri-add-line size-4 text-text-tertiary" />
</button>
<SelectDataset
isShow={isShowModal}
modal={modal}
onClose={hideModal}
onClose={() => setIsShowModal(false)}
selectedIds={selectedIds}
onSelect={handleSelect}
/>

View File

@ -11,7 +11,6 @@ import {
DrawerViewport,
} from '@langgenius/dify-ui/drawer'
import { RiDeleteBinLine, RiEditLine } from '@remixicon/react'
import { useBoolean } from 'ahooks'
import * as React from 'react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -53,15 +52,14 @@ const DatasetItem: FC<Props> = ({
const { formatIndexingTechniqueAndMethod } = useKnowledge()
const [isDeleteHovered, setIsDeleteHovered] = useState(false)
const [isShowSettingsModal, { setTrue: showSettingsModal, setFalse: hideSettingsModal }] =
useBoolean(false)
const [isShowSettingsModal, setIsShowSettingsModal] = useState(false)
const handleSave = useCallback(
(newDataset: DataSet) => {
onChange(newDataset)
hideSettingsModal()
setIsShowSettingsModal(false)
},
[hideSettingsModal, onChange],
[onChange],
)
const handleRemove = useCallback(
@ -104,7 +102,7 @@ const DatasetItem: FC<Props> = ({
aria-label={t(($) => $['operation.edit'], { ns: 'common' })}
onClick={(e) => {
e.stopPropagation()
showSettingsModal()
setIsShowSettingsModal(true)
}}
>
<RiEditLine className="size-4 shrink-0 text-text-tertiary" />
@ -150,7 +148,7 @@ const DatasetItem: FC<Props> = ({
modal
swipeDirection="right"
onOpenChange={(open) => {
if (!open) hideSettingsModal()
if (!open) setIsShowSettingsModal(false)
}}
>
<DrawerPortal>
@ -173,7 +171,7 @@ const DatasetItem: FC<Props> = ({
<SettingsModal
currentDataset={payload}
height={settingsModalHeight}
onCancel={hideSettingsModal}
onCancel={() => setIsShowSettingsModal(false)}
onSave={handleSave}
/>
</DrawerContent>

View File

@ -3,9 +3,8 @@ import type { FC } from 'react'
import type { ModelConfig } from '@/app/components/workflow/types'
import type { GenRes } from '@/service/debug'
import { cn } from '@langgenius/dify-ui/cn'
import { useBoolean } from 'ahooks'
import * as React from 'react'
import { useCallback } from 'react'
import { useCallback, useState } from 'react'
import GetAutomaticResModal from '@/app/components/app/configuration/config/automatic/get-automatic-res'
import { ActionButton } from '@/app/components/base/action-button'
import { Generator } from '@/app/components/base/icons/src/vender/other'
@ -28,26 +27,25 @@ const PromptGeneratorBtn: FC<Props> = ({
editorId,
currentPrompt,
}) => {
const [showAutomatic, { setTrue: showAutomaticTrue, setFalse: showAutomaticFalse }] =
useBoolean(false)
const [showAutomatic, setShowAutomatic] = useState(false)
const handleAutomaticRes = useCallback(
(res: GenRes) => {
onGenerated?.(res.modified)
showAutomaticFalse()
setShowAutomatic(false)
},
[onGenerated, showAutomaticFalse],
[onGenerated],
)
const configsMap = useHooksStore((s) => s.configsMap)
return (
<div className={cn(className)}>
<ActionButton className="hover:bg-[#155EFF]/8" onClick={showAutomaticTrue}>
<ActionButton className="hover:bg-[#155EFF]/8" onClick={() => setShowAutomatic(true)}>
<Generator className="size-4 text-primary-600" />
</ActionButton>
{showAutomatic && (
<GetAutomaticResModal
mode={AppModeEnum.CHAT}
isShow={showAutomatic}
onClose={showAutomaticFalse}
onClose={() => setShowAutomatic(false)}
onFinished={handleAutomaticRes}
flowId={configsMap?.flowId || ''}
nodeId={nodeId}

View File

@ -2,7 +2,7 @@
import type { SchemaRoot, StructuredOutput } from '../types'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { useBoolean } from 'ahooks'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import ShowPanel from '@/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/show'
import { Type } from '../types'
@ -16,7 +16,7 @@ type Props = Readonly<{
export function StructureOutput({ className, value, onChange }: Props) {
const { t } = useTranslation()
const [showConfig, { setTrue: showConfigModal, setFalse: hideConfigModal }] = useBoolean(false)
const [showConfig, setShowConfig] = useState(false)
function handleChange(value: SchemaRoot) {
onChange({
@ -31,7 +31,12 @@ export function StructureOutput({ className, value, onChange }: Props) {
<div className="code-sm-semibold text-text-secondary">structured_output</div>
<div className="ml-2 system-xs-regular text-text-tertiary">object</div>
</div>
<Button size="small" variant="secondary" className="flex" onClick={showConfigModal}>
<Button
size="small"
variant="secondary"
className="flex"
onClick={() => setShowConfig(true)}
>
<i className="mr-1 i-ri-edit-line size-3.5" aria-hidden="true" />
<div className="system-xs-medium text-components-button-secondary-text">
{t(($) => $['structOutput.configure'], { ns: 'app' })}
@ -46,7 +51,7 @@ export function StructureOutput({ className, value, onChange }: Props) {
<button
type="button"
className="mt-1.5 flex h-10 w-full cursor-pointer items-center justify-center rounded-[10px] bg-background-section system-xs-regular text-text-tertiary"
onClick={showConfigModal}
onClick={() => setShowConfig(true)}
>
{t(($) => $['structOutput.notConfiguredTip'], { ns: 'app' })}
</button>
@ -64,7 +69,7 @@ export function StructureOutput({ className, value, onChange }: Props) {
}) as any
} // wait for types change
onSave={handleChange as any} // wait for types change
onClose={hideConfigModal}
onClose={() => setShowConfig(false)}
/>
)}
</div>

View File

@ -1,87 +0,0 @@
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { ParamType } from '../../../types'
import List from '../list'
const createParam = (
overrides: Partial<{
name: string
type: ParamType
description: string
required: boolean
}> = {},
) => ({
name: 'city',
type: ParamType.string,
description: 'City name',
required: false,
...overrides,
})
describe('parameter-extractor/extract-parameter/list', () => {
it('renders the empty placeholder when no parameter is configured', () => {
render(<List readonly={false} list={[]} onChange={vi.fn()} />)
expect(
screen.getByText('workflow.nodes.parameterExtractor.extractParametersNotSet'),
).toBeInTheDocument()
})
// TODO: Fix this test.
// This test only failed in the merge queue, and I don't know why.
it.skip('edits and deletes parameters through the real item and modal flow', async () => {
const user = userEvent.setup()
const handleChange = vi.fn()
const { rerender } = render(
<List readonly={false} list={[createParam()]} onChange={handleChange} />,
)
const cityLabel = screen.getByText('city')
const cityRow = cityLabel.closest('.group.relative')
if (!cityRow) throw new Error('Failed to locate city row')
const editAndDeleteButtons = cityRow.querySelectorAll('.cursor-pointer.rounded-md.p-1')
fireEvent.click(editAndDeleteButtons[0] as HTMLElement)
await screen.findByRole('dialog')
const dialog = screen.getAllByRole('dialog').at(-1)!
fireEvent.change(
within(dialog).getByPlaceholderText(
'workflow.nodes.parameterExtractor.addExtractParameterContent.namePlaceholder',
),
{ target: { value: 'cityname' } },
)
fireEvent.change(
within(dialog).getByPlaceholderText(
'workflow.nodes.parameterExtractor.addExtractParameterContent.descriptionPlaceholder',
),
{ target: { value: 'Updated city description' } },
)
fireEvent.click(within(dialog).getByRole('button', { name: 'common.operation.save' }))
await waitFor(() => {
expect(handleChange).toHaveBeenCalled()
expect(handleChange.mock.lastCall?.[0]).toEqual([
{
name: 'cityname',
type: ParamType.string,
description: 'Updated city description',
required: false,
},
])
})
handleChange.mockClear()
rerender(
<List readonly={false} list={[createParam({ name: 'budget' })]} onChange={handleChange} />,
)
const budgetLabel = screen.getByText('budget')
const budgetRow = budgetLabel.closest('.group.relative')
if (!budgetRow) throw new Error('Failed to locate budget row')
const deleteButtons = budgetRow.querySelectorAll('.cursor-pointer.rounded-md.p-1')
fireEvent.click(deleteButtons[1] as HTMLElement)
expect(handleChange).toHaveBeenCalledWith([])
})
})

View File

@ -2,7 +2,6 @@
import type { FC } from 'react'
import type { Param } from '../../types'
import type { MoreInfo } from '@/app/components/workflow/types'
import { useBoolean } from 'ahooks'
import * as React from 'react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -20,7 +19,7 @@ type Props = Readonly<{
const List: FC<Props> = ({ list, onChange }) => {
const { t } = useTranslation()
const [isShowEditModal, { setTrue: showEditModal, setFalse: hideEditModal }] = useBoolean(false)
const [isShowEditModal, setIsShowEditModal] = useState(false)
const handleItemChange = useCallback(
(index: number) => {
@ -31,23 +30,20 @@ const List: FC<Props> = ({ list, onChange }) => {
return item
})
onChange(newList, moreInfo)
hideEditModal()
setIsShowEditModal(false)
}
},
[hideEditModal, list, onChange],
[list, onChange],
)
const [currEditItemIndex, setCurrEditItemIndex] = useState<number>(-1)
const handleItemEdit = useCallback(
(index: number) => {
return () => {
setCurrEditItemIndex(index)
showEditModal()
}
},
[showEditModal],
)
const handleItemEdit = useCallback((index: number) => {
return () => {
setCurrEditItemIndex(index)
setIsShowEditModal(true)
}
}, [])
const handleItemDelete = useCallback(
(index: number) => {
@ -81,7 +77,7 @@ const List: FC<Props> = ({ list, onChange }) => {
type="edit"
payload={list[currEditItemIndex]}
onSave={handleItemChange(currEditItemIndex)}
onCancel={hideEditModal}
onCancel={() => setIsShowEditModal(false)}
/>
)}
</div>

View File

@ -15,7 +15,6 @@ import {
import { Switch } from '@langgenius/dify-ui/switch'
import { Textarea } from '@langgenius/dify-ui/textarea'
import { toast } from '@langgenius/dify-ui/toast'
import { useBoolean } from 'ahooks'
import * as React from 'react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -92,18 +91,18 @@ const AddExtractParameter: FC<Props> = ({ type, payload, onSave, onCancel }) =>
[param.name, t],
)
const [isShowModal, { setTrue: doShowModal, setFalse: doHideModal }] = useBoolean(!isAdd)
const [isShowModal, setIsShowModal] = useState(!isAdd)
const hideModal = useCallback(() => {
doHideModal()
setIsShowModal(false)
onCancel?.()
}, [onCancel, doHideModal])
}, [onCancel])
const showAddModal = useCallback(() => {
if (isAdd) setParam(DEFAULT_PARAM)
doShowModal()
}, [isAdd, doShowModal])
setIsShowModal(true)
}, [isAdd])
const checkValid = useCallback(() => {
let errMessage = ''

View File

@ -3,10 +3,10 @@ import type { FC } from 'react'
import type { InputVar, MoreInfo } from '@/app/components/workflow/types'
import { cn } from '@langgenius/dify-ui/cn'
import { RiDeleteBinLine } from '@remixicon/react'
import { useBoolean, useHover } from 'ahooks'
import { useHover } from 'ahooks'
import { noop } from 'es-toolkit/function'
import * as React from 'react'
import { useCallback, useRef } from 'react'
import { useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ConfigVarModal from '@/app/components/app/configuration/config-var/config-modal'
import Badge from '@/app/components/base/badge'
@ -41,16 +41,15 @@ const VarItem: FC<Props> = ({
const ref = useRef(null)
const isHovering = useHover(ref)
const [isShowEditVarModal, { setTrue: showEditVarModal, setFalse: hideEditVarModal }] =
useBoolean(false)
const [isShowEditVarModal, setIsShowEditVarModal] = useState(false)
const handlePayloadChange = useCallback(
(payload: InputVar, moreInfo?: MoreInfo) => {
const isValid = onChange(payload, moreInfo)
if (!isValid) return
hideEditVarModal()
setIsShowEditVarModal(false)
},
[onChange, hideEditVarModal],
[onChange],
)
return (
<div
@ -107,7 +106,7 @@ const VarItem: FC<Props> = ({
type="button"
aria-label={t(($) => $['operation.edit'], { ns: 'common' })}
className="mr-1 cursor-pointer rounded-md border-none bg-transparent p-1 hover:bg-state-base-hover focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
onClick={showEditVarModal}
onClick={() => setIsShowEditVarModal(true)}
>
<Edit03 className="size-4 text-text-tertiary" aria-hidden="true" />
</button>
@ -133,7 +132,7 @@ const VarItem: FC<Props> = ({
isShow
supportFile
payload={payload}
onClose={hideEditVarModal}
onClose={() => setIsShowEditVarModal(false)}
onConfirm={handlePayloadChange}
varKeys={varKeys}
/>

View File

@ -1,7 +1,6 @@
import type { StartNodeType } from './types'
import type { InputVar, MoreInfo, ValueSelector } from '@/app/components/workflow/types'
import { toast } from '@langgenius/dify-ui/toast'
import { useBoolean } from 'ahooks'
import { produce } from 'immer'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -22,13 +21,8 @@ const useConfig = (id: string, payload: StartNodeType) => {
const { deleteNodeInspectorVars, renameInspectVarName, nodesWithInspectVars, deleteInspectVar } =
useInspectVarsCrud()
const [isShowAddVarModal, { setTrue: showAddVarModal, setFalse: hideAddVarModal }] =
useBoolean(false)
const [
isShowRemoveVarConfirm,
{ setTrue: showRemoveVarConfirm, setFalse: hideRemoveVarConfirm },
] = useBoolean(false)
const [isShowAddVarModal, setIsShowAddVarModal] = useState(false)
const [isShowRemoveVarConfirm, setIsShowRemoveVarConfirm] = useState(false)
const [removedVar, setRemovedVar] = useState<ValueSelector>([])
const [removedIndex, setRemoveIndex] = useState(0)
const handleVarListChange = useCallback(
@ -42,7 +36,7 @@ const useConfig = (id: string, payload: StartNodeType) => {
if (varId) deleteInspectVar(id, varId)
if (isVarUsedInNodes([id, moreInfo?.payload?.payload?.beforeKey || ''])) {
showRemoveVarConfirm()
setIsShowRemoveVarConfirm(true)
setRemovedVar([id, moreInfo?.payload?.payload?.beforeKey || ''])
setRemoveIndex(moreInfo?.index as number)
return
@ -76,7 +70,6 @@ const useConfig = (id: string, payload: StartNodeType) => {
nodesWithInspectVars,
renameInspectVarName,
setInputs,
showRemoveVarConfirm,
],
)
@ -86,8 +79,8 @@ const useConfig = (id: string, payload: StartNodeType) => {
})
setInputs(newInputs)
removeUsedVarInNodes(removedVar)
hideRemoveVarConfirm()
}, [hideRemoveVarConfirm, inputs, removeUsedVarInNodes, removedIndex, removedVar, setInputs])
setIsShowRemoveVarConfirm(false)
}, [inputs, removeUsedVarInNodes, removedIndex, removedVar, setInputs])
const handleAddVariable = useCallback(
(payload: InputVar) => {
@ -124,12 +117,12 @@ const useConfig = (id: string, payload: StartNodeType) => {
isChatMode,
inputs,
isShowAddVarModal,
showAddVarModal,
hideAddVarModal,
showAddVarModal: () => setIsShowAddVarModal(true),
hideAddVarModal: () => setIsShowAddVarModal(false),
handleVarListChange,
handleAddVariable,
isShowRemoveVarConfirm,
hideRemoveVarConfirm,
hideRemoveVarConfirm: () => setIsShowRemoveVarConfirm(false),
onRemoveVarConfirm: removeVarInNode,
}
}

View File

@ -1,9 +1,7 @@
import type { ToolVarInputs } from '../../../types'
import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { SchemaRoot } from '@/app/components/workflow/nodes/llm/types'
import { fireEvent, render, screen, within } from '@testing-library/react'
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { VarType } from '../../../types'
import ToolFormItem from '../item'
type MockSchemaModalProps = {
@ -13,22 +11,12 @@ type MockSchemaModalProps = {
schema: SchemaRoot
}
type MockFormInputItemProps = {
readOnly: boolean
nodeId: string
type MockFormInputItemProps = Readonly<{
schema: CredentialFormSchema
value: ToolVarInputs
onChange: (value: ToolVarInputs) => void
inPanel?: boolean
showManageInputField?: boolean
onManageInputField?: () => void
extraParams?: Record<string, unknown>
providerType?: 'tool' | 'trigger'
}
}>
const mockUseLanguage = vi.fn()
const mockSchemaModal = vi.fn<(props: MockSchemaModalProps) => void>()
const mockFormInputItem = vi.fn<(props: MockFormInputItemProps) => void>()
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
useLanguage: () => mockUseLanguage(),
@ -52,14 +40,7 @@ vi.mock(
)
vi.mock('@/app/components/workflow/nodes/_base/components/form-input-item', () => ({
default: (props: MockFormInputItemProps) => {
mockFormInputItem(props)
return (
<div data-testid="form-input-item" data-provider-type={props.providerType}>
{props.schema.variable}
</div>
)
},
default: ({ schema }: MockFormInputItemProps) => <div>{schema.variable}</div>,
}))
const createSchema = (overrides: Partial<CredentialFormSchema> = {}): CredentialFormSchema => ({
@ -85,50 +66,6 @@ describe('tool/tool-form/item', () => {
mockUseLanguage.mockReturnValue('en_US')
})
// Text input fields render their descriptions inline above the input.
it('should render text input labels and forward props to form input item', () => {
const handleChange = vi.fn()
const handleManageInputField = vi.fn()
const value: ToolVarInputs = {
api_key: {
type: VarType.constant,
value: 'secret',
},
}
render(
<ToolFormItem
readOnly
nodeId="tool-node"
schema={createSchema()}
value={value}
onChange={handleChange}
inPanel
showManageInputField
onManageInputField={handleManageInputField}
extraParams={{ mode: 'panel' }}
/>,
)
expect(screen.getByText('API Key'))!.toBeInTheDocument()
expect(screen.getByText('*'))!.toBeInTheDocument()
expect(screen.getByText('Enter API key'))!.toBeInTheDocument()
expect(screen.queryByRole('button', { name: 'JSON Schema' })).not.toBeInTheDocument()
expect(screen.getByTestId('form-input-item'))!.toHaveAttribute('data-provider-type', 'tool')
expect(mockFormInputItem.mock.calls[0]![0]).toMatchObject({
readOnly: true,
nodeId: 'tool-node',
schema: expect.objectContaining({ variable: 'api_key' }),
value,
onChange: handleChange,
inPanel: true,
showManageInputField: true,
onManageInputField: handleManageInputField,
extraParams: { mode: 'panel' },
providerType: 'tool',
})
})
// URL fragments inside descriptions should be rendered as external links.
it('should render URLs in descriptions as external links', () => {
render(
@ -197,8 +134,6 @@ describe('tool/tool-form/item', () => {
const schemaModal = screen.getByTestId('schema-modal')
expect(schemaModal)!.toBeInTheDocument()
expect(within(schemaModal).getByText('tool_config'))!.toBeInTheDocument()
expect(mockFormInputItem.mock.calls[0]![0].providerType).toBe('trigger')
fireEvent.click(screen.getByRole('button', { name: 'close-schema' }))
expect(screen.queryByTestId('schema-modal')).not.toBeInTheDocument()
})

View File

@ -5,7 +5,7 @@ import type { CredentialFormSchema } from '@/app/components/header/account-setti
import type { Tool } from '@/app/components/tools/types'
import type { ToolWithProvider } from '@/app/components/workflow/types'
import { Button } from '@langgenius/dify-ui/button'
import { useBoolean } from 'ahooks'
import { useState } from 'react'
import { Infotip } from '@/app/components/base/infotip'
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
@ -81,7 +81,7 @@ const ToolFormItem: FC<Props> = ({
const { name, label, type, required, tooltip, input_schema } = schema
const showSchemaButton = type === FormTypeEnum.object || type === FormTypeEnum.array
const showDescription = type === FormTypeEnum.textInput || type === FormTypeEnum.secretInput
const [isShowSchema, { setTrue: showSchema, setFalse: hideSchema }] = useBoolean(false)
const [isShowSchema, setIsShowSchema] = useState(false)
return (
<div className="space-y-0.5 py-1">
<div>
@ -107,7 +107,7 @@ const ToolFormItem: FC<Props> = ({
<Button
variant="ghost"
size="small"
onClick={showSchema}
onClick={() => setIsShowSchema(true)}
className="px-1 system-xs-regular text-text-tertiary"
>
<span aria-hidden className="mr-1 i-ri-braces-line size-3.5" />
@ -138,7 +138,12 @@ const ToolFormItem: FC<Props> = ({
/>
{isShowSchema && (
<SchemaModal isShow onClose={hideSchema} rootName={name} schema={input_schema!} />
<SchemaModal
isShow
onClose={() => setIsShowSchema(false)}
rootName={name}
schema={input_schema!}
/>
)}
</div>
)

View File

@ -1,7 +1,6 @@
import type { ToolNodeType, ToolVarInputs } from '../types'
import type { InputVar } from '@/app/components/workflow/types'
import { toast } from '@langgenius/dify-ui/toast'
import { useBoolean } from 'ahooks'
import { capitalize } from 'es-toolkit/string'
import { produce } from 'immer'
import { useCallback, useEffect, useMemo, useState } from 'react'
@ -43,7 +42,7 @@ const useConfig = (id: string, payload: ToolNodeType) => {
// Auth
const isShowAuthBtn = isToolAuthorizationRequired(provider_type, currCollection)
const [showSetAuth, { setTrue: showSetAuthModal, setFalse: hideSetAuthModal }] = useBoolean(false)
const [showSetAuth, setShowSetAuth] = useState(false)
const invalidToolsByType = useInvalidToolsByType(provider_type)
const handleSaveAuth = useCallback(
@ -52,9 +51,9 @@ const useConfig = (id: string, payload: ToolNodeType) => {
toast.success(t(($) => $['api.actionSuccess'], { ns: 'common' }))
invalidToolsByType()
hideSetAuthModal()
setShowSetAuth(false)
},
[currCollection?.name, hideSetAuthModal, t, invalidToolsByType],
[currCollection?.name, t, invalidToolsByType],
)
const currTool = useMemo(() => {
@ -233,8 +232,8 @@ const useConfig = (id: string, payload: ToolNodeType) => {
currCollection,
isShowAuthBtn,
showSetAuth,
showSetAuthModal,
hideSetAuthModal,
showSetAuthModal: () => setShowSetAuth(true),
hideSetAuthModal: () => setShowSetAuth(false),
handleSaveAuth,
isLoading,
outputSchema,

View File

@ -5,7 +5,7 @@ import type { Event } from '@/app/components/tools/types'
import type { TriggerWithProvider } from '@/app/components/workflow/block-selector/types'
import type { PluginTriggerVarInputs } from '@/app/components/workflow/nodes/trigger-plugin/types'
import { Button } from '@langgenius/dify-ui/button'
import { useBoolean } from 'ahooks'
import { useState } from 'react'
import { Infotip } from '@/app/components/base/infotip'
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
@ -41,7 +41,7 @@ const TriggerFormItem: FC<Props> = ({
const { name, label, type, required, tooltip, input_schema } = schema
const showSchemaButton = type === FormTypeEnum.object || type === FormTypeEnum.array
const showDescription = type === FormTypeEnum.textInput || type === FormTypeEnum.secretInput
const [isShowSchema, { setTrue: showSchema, setFalse: hideSchema }] = useBoolean(false)
const [isShowSchema, setIsShowSchema] = useState(false)
return (
<div className="space-y-0.5 py-1">
<div>
@ -67,7 +67,7 @@ const TriggerFormItem: FC<Props> = ({
<Button
variant="ghost"
size="small"
onClick={showSchema}
onClick={() => setIsShowSchema(true)}
className="px-1 system-xs-regular text-text-tertiary"
>
<span aria-hidden className="mr-1 i-ri-braces-line size-3.5" />
@ -97,7 +97,12 @@ const TriggerFormItem: FC<Props> = ({
/>
{isShowSchema && (
<SchemaModal isShow onClose={hideSchema} rootName={name} schema={input_schema!} />
<SchemaModal
isShow
onClose={() => setIsShowSchema(false)}
rootName={name}
schema={input_schema!}
/>
)}
</div>
)

View File

@ -21,20 +21,6 @@ vi.mock('uuid', () => ({
}))
vi.mock('ahooks', () => ({
useBoolean: (initialValue: boolean) => {
let current = initialValue
return [
current,
{
setTrue: () => {
current = true
},
setFalse: () => {
current = false
},
},
] as const
},
useDebounceFn: (fn: (...args: unknown[]) => void) => ({
run: fn,
}),
@ -114,16 +100,6 @@ describe('useConfig', () => {
mockIsVarUsedInNodes.mockReturnValue(false)
})
it('should expose read-only state, group mode and typed variable filters', () => {
const { result } = renderHook(() => useConfig('assigner-node', createPayload()))
expect(result.current.readOnly).toBe(false)
expect(result.current.isEnableGroup).toBe(true)
expect(result.current.filterVar(VarType.string)({ type: VarType.any } as never)).toBe(true)
expect(result.current.filterVar(VarType.number)({ type: VarType.string } as never)).toBe(false)
expect(result.current.getAvailableVars).toBe(mockGetAvailableVars)
})
it('should update root and grouped variable payloads', () => {
const { result } = renderHook(() => useConfig('assigner-node', createPayload()))

View File

@ -4,10 +4,9 @@ import type { VarGroupItem as VarGroupItemType } from '../types'
import type { NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types'
import { toast } from '@langgenius/dify-ui/toast'
import { RiDeleteBinLine } from '@remixicon/react'
import { useBoolean } from 'ahooks'
import { produce } from 'immer'
import * as React from 'react'
import { useCallback } from 'react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Folder } from '@/app/components/base/icons/src/vender/line/files'
import Field from '@/app/components/workflow/nodes/_base/components/field'
@ -92,8 +91,7 @@ const VarGroupItem: FC<Props> = ({
[payload.output_type],
)
const [isEditGroupName, { setTrue: setEditGroupName, setFalse: setNotEditGroupName }] =
useBoolean(false)
const [isEditGroupName, setIsEditGroupName] = useState(false)
const handleGroupNameChange = useCallback(
(e: ChangeEvent<any>) => {
@ -122,7 +120,7 @@ const VarGroupItem: FC<Props> = ({
{!isEditGroupName ? (
<div
className="flex h-6 cursor-text items-center rounded-lg px-1 system-sm-semibold text-text-secondary hover:bg-gray-100"
onClick={setEditGroupName}
onClick={() => setIsEditGroupName(true)}
>
{payload.group_name}
</div>
@ -137,7 +135,7 @@ const VarGroupItem: FC<Props> = ({
autoFocus
value={payload.group_name}
onChange={handleGroupNameChange}
onBlur={setNotEditGroupName}
onBlur={() => setIsEditGroupName(false)}
maxLength={30}
/>
)}

View File

@ -1,6 +1,6 @@
import type { ValueSelector } from '../../types'
import type { VarGroupItem, VariableAssignerNodeType } from './types'
import { useBoolean, useDebounceFn } from 'ahooks'
import { useDebounceFn } from 'ahooks'
import { useCallback, useRef, useState } from 'react'
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
import useInspectVarsCrud from '../../hooks/use-inspect-vars-crud'
@ -43,10 +43,7 @@ const useConfig = (id: string, payload: VariableAssignerNodeType) => {
const getAvailableVars = useGetAvailableVars()
const [
isShowRemoveVarConfirm,
{ setTrue: showRemoveVarConfirm, setFalse: hideRemoveVarConfirm },
] = useBoolean(false)
const [isShowRemoveVarConfirm, setIsShowRemoveVarConfirm] = useState(false)
const [removedVars, setRemovedVars] = useState<ValueSelector[]>([])
const [removeType, setRemoveType] = useState<'group' | 'enableChanged'>('group')
@ -60,7 +57,7 @@ const useConfig = (id: string, payload: VariableAssignerNodeType) => {
const groupName = groups[index]!.group_name
if (isVarUsedInNodes([id, groupName, 'output'])) {
showRemoveVarConfirm()
setIsShowRemoveVarConfirm(true)
setRemovedVars([[id, groupName, 'output']])
setRemoveType('group')
setRemovedGroupIndex(index)
@ -69,7 +66,7 @@ const useConfig = (id: string, payload: VariableAssignerNodeType) => {
setInputs(removeGroupByIndex(inputs, index))
}
},
[id, inputs, isVarUsedInNodes, setInputs, showRemoveVarConfirm],
[id, inputs, isVarUsedInNodes, setInputs],
)
const handleGroupEnabledChange = useCallback(
@ -86,7 +83,7 @@ const useConfig = (id: string, payload: VariableAssignerNodeType) => {
(item, index) => index > 0 && isVarUsedInNodes([id, item.group_name, 'output']),
)
if (useVars.length > 0) {
showRemoveVarConfirm()
setIsShowRemoveVarConfirm(true)
setRemovedVars(useVars.map((item) => [id, item.group_name, 'output']))
setRemoveType('enableChanged')
return
@ -99,15 +96,7 @@ const useConfig = (id: string, payload: VariableAssignerNodeType) => {
setInputs(toggleGroupEnabled({ inputs, enabled }))
deleteNodeInspectorVars(id)
},
[
deleteNodeInspectorVars,
handleOutVarRenameChange,
id,
inputs,
isVarUsedInNodes,
setInputs,
showRemoveVarConfirm,
],
[deleteNodeInspectorVars, handleOutVarRenameChange, id, inputs, isVarUsedInNodes, setInputs],
)
const handleAddGroup = useCallback(() => {
@ -148,22 +137,14 @@ const useConfig = (id: string, payload: VariableAssignerNodeType) => {
removedVars.forEach((v) => {
removeUsedVarInNodes(v)
})
hideRemoveVarConfirm()
setIsShowRemoveVarConfirm(false)
if (removeType === 'group') {
if (removedGroupIndex >= 0) setInputs(removeGroupByIndex(inputs, removedGroupIndex))
} else {
// removeType === 'enableChanged' to enabled
setInputs(toggleGroupEnabled({ inputs, enabled: false }))
}
}, [
removedVars,
hideRemoveVarConfirm,
removeType,
removeUsedVarInNodes,
inputs,
setInputs,
removedGroupIndex,
])
}, [removedVars, removeType, removeUsedVarInNodes, inputs, setInputs, removedGroupIndex])
return {
readOnly,
@ -176,7 +157,7 @@ const useConfig = (id: string, payload: VariableAssignerNodeType) => {
handleGroupRemoved,
handleVarGroupNameChange,
isShowRemoveVarConfirm,
hideRemoveVarConfirm,
hideRemoveVarConfirm: () => setIsShowRemoveVarConfirm(false),
onRemoveVarConfirm,
getAvailableVars,
filterVar: filterVarByType,