mirror of
https://github.com/langgenius/dify.git
synced 2026-07-30 00:39:34 +08:00
refactor(web): remove workflow node useBoolean usage (#39729)
This commit is contained in:
parent
e604fad630
commit
8946d7fbf4
@ -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
|
||||
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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([])
|
||||
})
|
||||
})
|
||||
@ -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>
|
||||
|
||||
@ -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 = ''
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
})
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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()))
|
||||
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user