The list action node adds methods to extract specific list objects (#10421)

Co-authored-by: luowei <glpat-EjySCyNjWiLqAED-YmwM>
Co-authored-by: crazywoola <427733928@qq.com>
Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com>
This commit is contained in:
Charlie.Wei 2024-11-11 12:10:21 +08:00 committed by GitHub
parent 55edd5047e
commit fbee41f8c7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 148 additions and 7 deletions

View File

@ -49,8 +49,14 @@ class Limit(BaseModel):
size: int = -1 size: int = -1
class ExtractConfig(BaseModel):
enabled: bool = False
serial: str = "1"
class ListOperatorNodeData(BaseNodeData): class ListOperatorNodeData(BaseNodeData):
variable: Sequence[str] = Field(default_factory=list) variable: Sequence[str] = Field(default_factory=list)
filter_by: FilterBy filter_by: FilterBy
order_by: OrderBy order_by: OrderBy
limit: Limit limit: Limit
extract_by: ExtractConfig

View File

@ -58,6 +58,10 @@ class ListOperatorNode(BaseNode[ListOperatorNodeData]):
if self.node_data.filter_by.enabled: if self.node_data.filter_by.enabled:
variable = self._apply_filter(variable) variable = self._apply_filter(variable)
# Extract
if self.node_data.extract_by.enabled:
variable = self._extract_slice(variable)
# Order # Order
if self.node_data.order_by.enabled: if self.node_data.order_by.enabled:
variable = self._apply_order(variable) variable = self._apply_order(variable)
@ -140,6 +144,16 @@ class ListOperatorNode(BaseNode[ListOperatorNodeData]):
result = variable.value[: self.node_data.limit.size] result = variable.value[: self.node_data.limit.size]
return variable.model_copy(update={"value": result}) return variable.model_copy(update={"value": result})
def _extract_slice(
self, variable: Union[ArrayFileSegment, ArrayNumberSegment, ArrayStringSegment]
) -> Union[ArrayFileSegment, ArrayNumberSegment, ArrayStringSegment]:
value = int(self.graph_runtime_state.variable_pool.convert_template(self.node_data.extract_by.serial).text) - 1
if len(variable.value) > int(value):
result = variable.value[value]
else:
result = ""
return variable.model_copy(update={"value": [result]})
def _get_file_extract_number_func(*, key: str) -> Callable[[File], int]: def _get_file_extract_number_func(*, key: str) -> Callable[[File], int]:
match key: match key:

View File

@ -4,7 +4,14 @@ import pytest
from core.file import File, FileTransferMethod, FileType from core.file import File, FileTransferMethod, FileType
from core.variables import ArrayFileSegment from core.variables import ArrayFileSegment
from core.workflow.nodes.list_operator.entities import FilterBy, FilterCondition, Limit, ListOperatorNodeData, OrderBy from core.workflow.nodes.list_operator.entities import (
ExtractConfig,
FilterBy,
FilterCondition,
Limit,
ListOperatorNodeData,
OrderBy,
)
from core.workflow.nodes.list_operator.exc import InvalidKeyError from core.workflow.nodes.list_operator.exc import InvalidKeyError
from core.workflow.nodes.list_operator.node import ListOperatorNode, _get_file_extract_string_func from core.workflow.nodes.list_operator.node import ListOperatorNode, _get_file_extract_string_func
from models.workflow import WorkflowNodeExecutionStatus from models.workflow import WorkflowNodeExecutionStatus
@ -22,6 +29,7 @@ def list_operator_node():
), ),
"order_by": OrderBy(enabled=False, value="asc"), "order_by": OrderBy(enabled=False, value="asc"),
"limit": Limit(enabled=False, size=0), "limit": Limit(enabled=False, size=0),
"extract_by": ExtractConfig(enabled=False, serial="1"),
"title": "Test Title", "title": "Test Title",
} }
node_data = ListOperatorNodeData(**config) node_data = ListOperatorNodeData(**config)

View File

@ -0,0 +1,51 @@
'use client'
import type { FC } from 'react'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { VarType } from '../../../types'
import type { Var } from '../../../types'
import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
import cn from '@/utils/classnames'
import Input from '@/app/components/workflow/nodes/_base/components/input-support-select-var'
type Props = {
nodeId: string
readOnly: boolean
value: string
onChange: (value: string) => void
}
const ExtractInput: FC<Props> = ({
nodeId,
readOnly,
value,
onChange,
}) => {
const { t } = useTranslation()
const [isFocus, setIsFocus] = useState(false)
const { availableVars, availableNodesWithParent } = useAvailableVarList(nodeId, {
onlyLeafNodeVar: false,
filterVar: (varPayload: Var) => {
return [VarType.number].includes(varPayload.type)
},
})
return (
<div className='flex items-start space-x-1'>
<Input
instanceId='http-extract-number'
className={cn(isFocus ? 'shadow-xs bg-gray-50 border-gray-300' : 'bg-gray-100 border-gray-100', 'w-0 grow rounded-lg px-3 py-[6px] border')}
value={value}
onChange={onChange}
readOnly={readOnly}
nodesOutputVars={availableVars}
availableNodes={availableNodesWithParent}
onFocusChange={setIsFocus}
placeholder={!readOnly ? t('workflow.nodes.http.extractListPlaceholder')! : ''}
placeholderClassName='!leading-[21px]'
/>
</div >
)
}
export default React.memo(ExtractInput)

View File

@ -12,6 +12,10 @@ const nodeDefault: NodeDefault<ListFilterNodeType> = {
enabled: false, enabled: false,
conditions: [], conditions: [],
}, },
extract_by: {
enabled: false,
serial: '1',
},
order_by: { order_by: {
enabled: false, enabled: false,
key: '', key: '',

View File

@ -13,6 +13,7 @@ import FilterCondition from './components/filter-condition'
import Field from '@/app/components/workflow/nodes/_base/components/field' import Field from '@/app/components/workflow/nodes/_base/components/field'
import { type NodePanelProps } from '@/app/components/workflow/types' import { type NodePanelProps } from '@/app/components/workflow/types'
import Switch from '@/app/components/base/switch' import Switch from '@/app/components/base/switch'
import ExtractInput from '@/app/components/workflow/nodes/list-operator/components/extract-input'
const i18nPrefix = 'workflow.nodes.listFilter' const i18nPrefix = 'workflow.nodes.listFilter'
@ -32,6 +33,8 @@ const Panel: FC<NodePanelProps<ListFilterNodeType>> = ({
filterVar, filterVar,
handleFilterEnabledChange, handleFilterEnabledChange,
handleFilterChange, handleFilterChange,
handleExtractsEnabledChange,
handleExtractsChange,
handleLimitChange, handleLimitChange,
handleOrderByEnabledChange, handleOrderByEnabledChange,
handleOrderByKeyChange, handleOrderByKeyChange,
@ -79,6 +82,41 @@ const Panel: FC<NodePanelProps<ListFilterNodeType>> = ({
: null} : null}
</Field> </Field>
<Split /> <Split />
<Field
title={t(`${i18nPrefix}.extractsCondition`)}
operations={
<Switch
defaultValue={inputs.extract_by?.enabled}
onChange={handleExtractsEnabledChange}
size='md'
disabled={readOnly}
/>
}
>
{inputs.extract_by?.enabled
? (
<div className='flex items-center justify-between'>
{hasSubVariable && (
<div className='grow mr-2'>
<ExtractInput
value={inputs.extract_by.serial as string}
onChange={handleExtractsChange}
readOnly={readOnly}
nodeId={id}
/>
</div>
)}
</div>
)
: null}
</Field>
<Split />
<LimitConfig
config={inputs.limit}
onChange={handleLimitChange}
readonly={readOnly}
/>
<Split />
<Field <Field
title={t(`${i18nPrefix}.orderBy`)} title={t(`${i18nPrefix}.orderBy`)}
operations={ operations={
@ -118,13 +156,7 @@ const Panel: FC<NodePanelProps<ListFilterNodeType>> = ({
: null} : null}
</Field> </Field>
<Split /> <Split />
<LimitConfig
config={inputs.limit}
onChange={handleLimitChange}
readonly={readOnly}
/>
</div> </div>
<Split />
<div className='px-4 pt-4 pb-2'> <div className='px-4 pt-4 pb-2'>
<OutputVars> <OutputVars>
<> <>

View File

@ -25,6 +25,10 @@ export type ListFilterNodeType = CommonNodeType & {
enabled: boolean enabled: boolean
conditions: Condition[] conditions: Condition[]
} }
extract_by: {
enabled: boolean
serial?: string
}
order_by: { order_by: {
enabled: boolean enabled: boolean
key: ValueSelector | string key: ValueSelector | string

View File

@ -119,6 +119,22 @@ const useConfig = (id: string, payload: ListFilterNodeType) => {
setInputs(newInputs) setInputs(newInputs)
}, [inputs, setInputs]) }, [inputs, setInputs])
const handleExtractsEnabledChange = useCallback((enabled: boolean) => {
const newInputs = produce(inputs, (draft) => {
draft.extract_by.enabled = enabled
if (enabled)
draft.extract_by.serial = '1'
})
setInputs(newInputs)
}, [inputs, setInputs])
const handleExtractsChange = useCallback((value: string) => {
const newInputs = produce(inputs, (draft) => {
draft.extract_by.serial = value
})
setInputs(newInputs)
}, [inputs, setInputs])
const handleOrderByEnabledChange = useCallback((enabled: boolean) => { const handleOrderByEnabledChange = useCallback((enabled: boolean) => {
const newInputs = produce(inputs, (draft) => { const newInputs = produce(inputs, (draft) => {
draft.order_by.enabled = enabled draft.order_by.enabled = enabled
@ -162,6 +178,8 @@ const useConfig = (id: string, payload: ListFilterNodeType) => {
handleOrderByEnabledChange, handleOrderByEnabledChange,
handleOrderByKeyChange, handleOrderByKeyChange,
handleOrderByTypeChange, handleOrderByTypeChange,
handleExtractsEnabledChange,
handleExtractsChange,
} }
} }

View File

@ -369,6 +369,7 @@ const translation = {
inputVars: 'Input Variables', inputVars: 'Input Variables',
api: 'API', api: 'API',
apiPlaceholder: 'Enter URL, type / insert variable', apiPlaceholder: 'Enter URL, type / insert variable',
extractListPlaceholder: 'Enter list item index, type / insert variable',
notStartWithHttp: 'API should start with http:// or https://', notStartWithHttp: 'API should start with http:// or https://',
key: 'Key', key: 'Key',
type: 'Type', type: 'Type',
@ -605,6 +606,7 @@ const translation = {
inputVar: 'Input Variable', inputVar: 'Input Variable',
filterCondition: 'Filter Condition', filterCondition: 'Filter Condition',
filterConditionKey: 'Filter Condition Key', filterConditionKey: 'Filter Condition Key',
extractsCondition: 'Extract the N item',
filterConditionComparisonOperator: 'Filter Condition Comparison Operator', filterConditionComparisonOperator: 'Filter Condition Comparison Operator',
filterConditionComparisonValue: 'Filter Condition value', filterConditionComparisonValue: 'Filter Condition value',
selectVariableKeyPlaceholder: 'Select sub variable key', selectVariableKeyPlaceholder: 'Select sub variable key',

View File

@ -369,6 +369,7 @@ const translation = {
inputVars: '输入变量', inputVars: '输入变量',
api: 'API', api: 'API',
apiPlaceholder: '输入 URL输入变量时请键入/', apiPlaceholder: '输入 URL输入变量时请键入/',
extractListPlaceholder: '输入提取列表编号,输入变量时请键入‘/',
notStartWithHttp: 'API 应该以 http:// 或 https:// 开头', notStartWithHttp: 'API 应该以 http:// 或 https:// 开头',
key: '键', key: '键',
type: '类型', type: '类型',
@ -608,6 +609,7 @@ const translation = {
filterConditionComparisonOperator: '过滤条件比较操作符', filterConditionComparisonOperator: '过滤条件比较操作符',
filterConditionComparisonValue: '过滤条件比较值', filterConditionComparisonValue: '过滤条件比较值',
selectVariableKeyPlaceholder: '选择子变量的 Key', selectVariableKeyPlaceholder: '选择子变量的 Key',
extractsCondition: '取第 N 项',
limit: '取前 N 项', limit: '取前 N 项',
orderBy: '排序', orderBy: '排序',
asc: '升序', asc: '升序',