Co-authored-by: luowei <glpat-EjySCyNjWiLqAED-YmwM> Co-authored-by: crazywoola <427733928@qq.com> Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com>tags/0.11.1
| @@ -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 | |||
| @@ -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: | |||
| @@ -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) | |||
| @@ -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) | |||
| @@ -12,6 +12,10 @@ const nodeDefault: NodeDefault<ListFilterNodeType> = { | |||
| enabled: false, | |||
| conditions: [], | |||
| }, | |||
| extract_by: { | |||
| enabled: false, | |||
| serial: '1', | |||
| }, | |||
| order_by: { | |||
| enabled: false, | |||
| key: '', | |||
| @@ -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> | |||
| <> | |||
| @@ -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 | |||
| @@ -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, | |||
| } | |||
| } | |||
| @@ -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', | |||
| @@ -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: '升序', | |||