Browse Source

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>
Charlie.Wei 4 months ago
parent
commit
fbee41f8c7

+ 6 - 0
api/core/workflow/nodes/list_operator/entities.py

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

+ 14 - 0
api/core/workflow/nodes/list_operator/node.py

@@ -58,6 +58,10 @@ class ListOperatorNode(BaseNode[ListOperatorNodeData]):
             if self.node_data.filter_by.enabled:
                 variable = self._apply_filter(variable)
 
+            # Extract
+            if self.node_data.extract_by.enabled:
+                variable = self._extract_slice(variable)
+
             # Order
             if self.node_data.order_by.enabled:
                 variable = self._apply_order(variable)
@@ -140,6 +144,16 @@ class ListOperatorNode(BaseNode[ListOperatorNodeData]):
         result = variable.value[: self.node_data.limit.size]
         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]:
     match key:

+ 9 - 1
api/tests/unit_tests/core/workflow/nodes/test_list_operator.py

@@ -4,7 +4,14 @@ import pytest
 
 from core.file import File, FileTransferMethod, FileType
 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.node import ListOperatorNode, _get_file_extract_string_func
 from models.workflow import WorkflowNodeExecutionStatus
@@ -22,6 +29,7 @@ def list_operator_node():
         ),
         "order_by": OrderBy(enabled=False, value="asc"),
         "limit": Limit(enabled=False, size=0),
+        "extract_by": ExtractConfig(enabled=False, serial="1"),
         "title": "Test Title",
     }
     node_data = ListOperatorNodeData(**config)

+ 51 - 0
web/app/components/workflow/nodes/list-operator/components/extract-input.tsx

@@ -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)

+ 4 - 0
web/app/components/workflow/nodes/list-operator/default.ts

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

+ 38 - 6
web/app/components/workflow/nodes/list-operator/panel.tsx

@@ -13,6 +13,7 @@ import FilterCondition from './components/filter-condition'
 import Field from '@/app/components/workflow/nodes/_base/components/field'
 import { type NodePanelProps } from '@/app/components/workflow/types'
 import Switch from '@/app/components/base/switch'
+import ExtractInput from '@/app/components/workflow/nodes/list-operator/components/extract-input'
 
 const i18nPrefix = 'workflow.nodes.listFilter'
 
@@ -32,6 +33,8 @@ const Panel: FC<NodePanelProps<ListFilterNodeType>> = ({
     filterVar,
     handleFilterEnabledChange,
     handleFilterChange,
+    handleExtractsEnabledChange,
+    handleExtractsChange,
     handleLimitChange,
     handleOrderByEnabledChange,
     handleOrderByKeyChange,
@@ -79,6 +82,41 @@ const Panel: FC<NodePanelProps<ListFilterNodeType>> = ({
             : null}
         </Field>
         <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
           title={t(`${i18nPrefix}.orderBy`)}
           operations={
@@ -118,13 +156,7 @@ const Panel: FC<NodePanelProps<ListFilterNodeType>> = ({
             : null}
         </Field>
         <Split />
-        <LimitConfig
-          config={inputs.limit}
-          onChange={handleLimitChange}
-          readonly={readOnly}
-        />
       </div>
-      <Split />
       <div className='px-4 pt-4 pb-2'>
         <OutputVars>
           <>

+ 4 - 0
web/app/components/workflow/nodes/list-operator/types.ts

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

+ 18 - 0
web/app/components/workflow/nodes/list-operator/use-config.ts

@@ -119,6 +119,22 @@ const useConfig = (id: string, payload: ListFilterNodeType) => {
     setInputs(newInputs)
   }, [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 newInputs = produce(inputs, (draft) => {
       draft.order_by.enabled = enabled
@@ -162,6 +178,8 @@ const useConfig = (id: string, payload: ListFilterNodeType) => {
     handleOrderByEnabledChange,
     handleOrderByKeyChange,
     handleOrderByTypeChange,
+    handleExtractsEnabledChange,
+    handleExtractsChange,
   }
 }
 

+ 2 - 0
web/i18n/en-US/workflow.ts

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

+ 2 - 0
web/i18n/zh-Hans/workflow.ts

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