/
h0tnanny
/
IotPlatform
Обзор
Документация
Войти
/
h0tnanny
/
IotPlatform
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
develop
client/src/components/WorkflowEditor/nodes/LoopNode.tsx
281 строка
11 KB
h0tnanny
fix readonly permissions
01 фев 2026, 23:32
01 фев 2026, 23:32
01880f6
Код
Авторство
О чём код?
import { memo, useState, useMemo, useEffect } from 'react'; import { Handle, Position, NodeProps } from '@xyflow/react'; import { Repeat, Trash2 } from 'lucide-react'; import type { Field, FieldType, ComparisonOperator } from '../../../types/api.types'; import styles from './CustomNode.module.scss'; interface ConditionNodeInfo { id: string; label: string; } interface LoopNodeData { label: string; conditionType?: 'variable' | 'conditionNode'; // Тип условия: переменная или существующий Condition узел conditionNodeId?: string; // ID Condition узла (если conditionType === 'conditionNode') leftVariable?: string; operator?: ComparisonOperator; rightVariable?: string; maxIterations?: number; variables?: Field[]; availableConditionNodes?: ConditionNodeInfo[]; // Список доступных Condition узлов onUpdate: (data: any) => void; onDelete: () => void; isViewOnly?: boolean; } export const LoopNodeComponent = memo(({ data }: NodeProps) => { const typedData = data as unknown as LoopNodeData; const [label, setLabel] = useState(typedData.label || 'Loop'); const [conditionType, setConditionType] = useState<'variable' | 'conditionNode'>( typedData.conditionType || 'variable' ); const [conditionNodeId, setConditionNodeId] = useState(typedData.conditionNodeId || ''); const [leftVariable, setLeftVariable] = useState(typedData.leftVariable || ''); const [operator, setOperator] = useState<ComparisonOperator>(typedData.operator || '<'); const [rightVariable, setRightVariable] = useState(typedData.rightVariable || ''); const [maxIterations, setMaxIterations] = useState( String(typedData.maxIterations || 1000) ); const variables = typedData.variables || []; const availableConditionNodes = typedData.availableConditionNodes || []; const isViewOnly = typedData.isViewOnly ?? false; // Автоматическое обновление при изменении useEffect(() => { if (!isViewOnly) { typedData.onUpdate({ label, conditionType, conditionNodeId: conditionType === 'conditionNode' ? conditionNodeId : undefined, leftVariable: conditionType === 'variable' ? leftVariable : undefined, operator: conditionType === 'variable' ? operator : undefined, rightVariable: conditionType === 'variable' ? rightVariable : undefined, maxIterations: Number(maxIterations), }); } }, [label, conditionType, conditionNodeId, leftVariable, operator, rightVariable, maxIterations, isViewOnly]); // Получаем тип переменной по имени const getVariableType = (varName: string): FieldType | null => { const variable = variables.find(v => v.name === varName); return variable ? variable.type : null; }; // Проверяем совместимость типов для сравнения const areTypesCompatible = (type1: FieldType | null, type2: FieldType | null): boolean => { if (!type1 || !type2) return false; const numericTypes: FieldType[] = ['Number', 'Double']; if (numericTypes.includes(type1) && numericTypes.includes(type2)) { return true; } return type1 === type2; }; // Получаем доступные операторы const getAvailableOperators = (type: FieldType | null): ComparisonOperator[] => { if (!type) return ['==', '!=']; if (type === 'Boolean' || type === 'String') { return ['==', '!=']; } return ['==', '!=', '>', '<', '>=', '<=']; }; // Доступные операторы на основе выбранной левой переменной const availableOperators = useMemo(() => { const leftType = getVariableType(leftVariable); return getAvailableOperators(leftType); }, [leftVariable, variables]); return ( <div className={`${styles.customNode} ${styles.loopNode}`}> <Handle type="target" position={Position.Top} className={styles.handle} /> <div className={styles.nodeHeader}> <div className={styles.nodeIcon}> <Repeat size={16} /> </div> <div className={styles.nodeTitle}>{label || 'Loop'}</div> {!isViewOnly && ( <div className={styles.nodeActions}> <button className={styles.iconBtn} onClick={() => typedData.onDelete()} title="Удалить (Del)" > <Trash2 size={14} /> </button> </div> )} </div> <div className={styles.nodeEditor}> <input type="text" value={label} onChange={(e) => !isViewOnly && setLabel(e.target.value)} placeholder="Название" className={styles.input} readOnly={isViewOnly} /> {/* Тип условия */} <label className={styles.fieldLabel}>Тип условия:</label> <div className={styles.radioGroup}> <label className={styles.radioLabel}> <input type="radio" value="variable" checked={conditionType === 'variable'} onChange={(e) => !isViewOnly && setConditionType(e.target.value as 'variable')} disabled={isViewOnly} /> <span>Сравнение переменных</span> </label> <label className={styles.radioLabel}> <input type="radio" value="conditionNode" checked={conditionType === 'conditionNode'} onChange={(e) => !isViewOnly && setConditionType(e.target.value as 'conditionNode')} disabled={isViewOnly} /> <span>Использовать Condition узел</span> </label> </div> {conditionType === 'conditionNode' ? ( // Выбор существующего Condition узла <> <label className={styles.fieldLabel}>Выберите Condition узел:</label> <select value={conditionNodeId} onChange={(e) => !isViewOnly && setConditionNodeId(e.target.value)} className={styles.select} disabled={isViewOnly} > <option value="">Выберите узел</option> {availableConditionNodes.map((node) => ( <option key={node.id} value={node.id}> {node.label} </option> ))} </select> {availableConditionNodes.length === 0 && ( <div className={styles.hint}> Нет доступных Condition узлов. Создайте Condition узел сначала. </div> )} </> ) : ( // Сравнение переменных <> <label className={styles.fieldLabel}>Левая переменная:</label> <select value={leftVariable} onChange={(e) => !isViewOnly && setLeftVariable(e.target.value)} className={styles.select} disabled={isViewOnly} > <option value="">Выберите переменную</option> {variables.map((variable, index) => ( <option key={index} value={variable.name}> {variable.description || variable.name} ({variable.type}) </option> ))} </select> <label className={styles.fieldLabel}>Оператор:</label> <select value={operator} onChange={(e) => !isViewOnly && setOperator(e.target.value as ComparisonOperator)} className={styles.select} disabled={isViewOnly || !leftVariable} > {availableOperators.map(op => { const labels: Record<ComparisonOperator, string> = { '==': '== (равно)', '!=': '!= (не равно)', '>': '> (больше)', '<': '< (меньше)', '>=': '>= (больше или равно)', '<=': '<= (меньше или равно)', }; return ( <option key={op} value={op}> {labels[op]} </option> ); })} </select> <label className={styles.fieldLabel}>Правая переменная:</label> <select value={rightVariable} onChange={(e) => !isViewOnly && setRightVariable(e.target.value)} className={styles.select} disabled={isViewOnly || !leftVariable} > <option value="">Выберите переменную</option> {variables .filter(v => { const leftType = getVariableType(leftVariable); return leftType ? areTypesCompatible(leftType, v.type) : true; }) .map((variable, index) => ( <option key={index} value={variable.name}> {variable.description || variable.name} ({variable.type}) </option> ))} </select> </> )} {/* Максимальное количество итераций */} <label className={styles.fieldLabel}>Макс. итераций:</label> <input type="number" value={maxIterations} onChange={(e) => !isViewOnly && setMaxIterations(e.target.value)} placeholder="1000" min="1" className={styles.input} readOnly={isViewOnly} /> </div> <div className={styles.loopHandles}> <div className={styles.handleLabel}> <span className={styles.bodyLabel}>Body</span> <Handle type="source" position={Position.Bottom} id="body" className={`${styles.handle} ${styles.handleBody}`} style={{ left: '30%' }} /> </div> <div className={styles.handleLabel}> <span className={styles.exitLabel}>Exit</span> <Handle type="source" position={Position.Bottom} id="exit" className={`${styles.handle} ${styles.handleExit}`} style={{ left: '70%' }} /> </div> </div> </div> ); }); LoopNodeComponent.displayName = 'LoopNode';