feat(web): if-else node show cases
This commit is contained in:
@@ -1,26 +1,49 @@
|
|||||||
|
import { useRef } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import type { ReactShapeConfig } from '@antv/x6-react-shape';
|
import type { ReactShapeConfig } from '@antv/x6-react-shape';
|
||||||
import { Flex } from 'antd';
|
import { Flex } from 'antd';
|
||||||
|
|
||||||
import NodeTools from './NodeTools'
|
import NodeTools from './NodeTools'
|
||||||
|
import { useVariableList } from '../Properties/hooks/useVariableList'
|
||||||
|
|
||||||
const caculateIsSet = (item: any, type: string) => {
|
const caculateIsSet = (item: any, type: string) => {
|
||||||
switch(type) {
|
switch (type) {
|
||||||
case 'categories':
|
case 'categories':
|
||||||
return typeof item?.class_name === 'string' && item?.class_name !== ''
|
return typeof item?.class_name === 'string' && item?.class_name !== ''
|
||||||
case 'cases':
|
case 'cases': {
|
||||||
return item.expressions.length > 0 && item.expressions.filter((vo: any) => {
|
if (!item.left) return false
|
||||||
const keys = Object.keys(vo)
|
if (['not_empty', 'empty'].includes(item.operator)) return true
|
||||||
return keys.length === 0 || (keys.length > 0
|
return !!item.left && (!!item.right || typeof item.right === 'boolean')
|
||||||
&& ((['not_empty', 'empty'].includes(vo.operator) && (['undefined', 'null'].includes(typeof vo.left) || vo.left === ''))
|
}
|
||||||
|| (!['not_empty', 'empty'].includes(vo.operator) && (['undefined', 'null'].includes(typeof vo.right) || vo.right === ''))))
|
|
||||||
}).length === 0
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const ConditionNode: ReactShapeConfig['component'] = ({ node }) => {
|
const ConditionNode: ReactShapeConfig['component'] = ({ node }) => {
|
||||||
const data = node?.getData() || {};
|
const data = node?.getData() || {};
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const graphRef = useRef(node?.model?.graph)
|
||||||
|
const variableList = useVariableList(node ?? null, graphRef, [])
|
||||||
|
|
||||||
|
const getLocaleField = (field: string, filedType: string) => {
|
||||||
|
const key = filedType === 'boolean' ? `workflow.config.if-else..boolean.${field}` : filedType === 'number' ? `workflow.config.if-else.num.${field}` : `workflow.config.if-else.${field}`
|
||||||
|
const value = t(key)
|
||||||
|
return value !== key ? value : t(`workflow.config.if-else.num.${field}`)
|
||||||
|
};
|
||||||
|
const labelRender = (value: string) => {
|
||||||
|
const filterOption = variableList.find(vo => `{{${vo.value}}}` === value)
|
||||||
|
|
||||||
|
if (filterOption) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className="rb:max-w-[40%] rb:break-all rb:line-clamp-1 rb:text-[#155EEF]"
|
||||||
|
contentEditable={false}
|
||||||
|
>
|
||||||
|
{`{x}`} {filterOption.label}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx('rb:cursor-pointer rb:group rb:relative rb:h-full rb:w-full rb:p-3 rb:border rb:rounded-2xl rb:bg-[#FCFCFD] rb:shadow-[0px_2px_4px_0px_rgba(23,23,25,0.03)]', {
|
<div className={clsx('rb:cursor-pointer rb:group rb:relative rb:h-full rb:w-full rb:p-3 rb:border rb:rounded-2xl rb:bg-[#FCFCFD] rb:shadow-[0px_2px_4px_0px_rgba(23,23,25,0.03)]', {
|
||||||
@@ -48,16 +71,33 @@ const ConditionNode: ReactShapeConfig['component'] = ({ node }) => {
|
|||||||
{data.type === 'if-else' &&
|
{data.type === 'if-else' &&
|
||||||
<Flex vertical gap={4} className="rb:mt-3!">
|
<Flex vertical gap={4} className="rb:mt-3!">
|
||||||
{data.config?.cases?.defaultValue.map((item: any, index: number) => (
|
{data.config?.cases?.defaultValue.map((item: any, index: number) => (
|
||||||
<div key={index} className="rb:bg-[#F0F3F8] rb:shadow-[0px_2px_4px_0px_rgba(23,23,25,0.03)] rb:rounded-md rb:py-1 rb:px-1.5 rb:text-[10px] rb:text-[#5B6167] rb:font-medium rb:leading-3.5">
|
<div key={index} className={item.expressions.length > 0 ? '' : 'rb:mb-1'}>
|
||||||
<Flex justify="space-between">
|
<Flex justify={item.expressions.length > 0 ? "space-between" : 'end'} className="rb:mb-1">
|
||||||
<span>{index === 0 ? 'IF' : `ELIF`}</span>
|
{item.expressions.length > 0 && <span className="rb:text-[#5B6167] rb:text-[10px]">CASE{index + 1}</span>}
|
||||||
{caculateIsSet(item, 'cases') ? t(`workflow.config.${data.type}.set`) : t(`workflow.config.${data.type}.unset`)}
|
<span className="rb:text-[#212332] rb:font-medium rb:text-[12px]">{index === 0 ? 'IF' : `ELIF`}</span>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
{item.expressions.length > 0 && <Flex vertical gap={2}>
|
||||||
|
{item.expressions.map((expression: any, eIndex: number) => (
|
||||||
|
<div key={eIndex} className="rb:relative">
|
||||||
|
{item.expressions.length > 1 && eIndex > 0 && <div className="rb:absolute rb:-top-2 rb:right-2 rb:text-[10px] rb:text-[#155EEF] rb:font-medium rb:leading-3.5 rb:text-right rb:pr-0.5">{item.logical_operator.toLocaleUpperCase()}</div>}
|
||||||
|
<Flex align="center" className="rb:bg-[#F0F3F8] rb:shadow-[0px_2px_4px_0px_rgba(23,23,25,0.03)] rb:rounded-md rb:py-1! rb:px-1.5! rb:text-[10px] rb:text-[#5B6167] rb:font-medium rb:leading-3.5">
|
||||||
|
{caculateIsSet(expression, 'cases')
|
||||||
|
? <>
|
||||||
|
{labelRender(expression.left)}
|
||||||
|
<span className="rb:mx-1">{getLocaleField(expression.operator, typeof expression.right)}</span>
|
||||||
|
<span className="rb:break-all rb:line-clamp-1">{!['not_empty', 'empty'].includes(expression.operator) && <span>{typeof expression.right === 'boolean' ? String(expression.right).charAt(0).toUpperCase() + String(expression.right).slice(1) : expression.right}</span>}</span>
|
||||||
|
</>
|
||||||
|
: t(`workflow.config.${data.type}.unset`)
|
||||||
|
}
|
||||||
|
</Flex>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Flex>}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div className="rb:bg-[#F0F3F8] rb:shadow-[0px_2px_4px_0px_rgba(23,23,25,0.03)] rb:rounded-md rb:py-1 rb:px-1.5 rb:text-[10px] rb:text-[#5B6167] rb:font-medium rb:leading-3.5">
|
<Flex justify="end" className="rb:text-[#212332] rb:font-medium rb:text-[12px]">
|
||||||
ELSE
|
ELSE
|
||||||
</div>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-09 18:24:53
|
* @Date: 2026-02-09 18:24:53
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-03-06 14:24:57
|
* @Last Modified time: 2026-03-24 15:00:46
|
||||||
*/
|
*/
|
||||||
import { type FC } from 'react'
|
import { type FC } from 'react'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
@@ -12,9 +12,10 @@ import { Form, Button, Select, Space, Divider, InputNumber, type SelectProps, Fl
|
|||||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||||
import VariableSelect from '../VariableSelect'
|
import VariableSelect from '../VariableSelect'
|
||||||
import Editor from '../../Editor'
|
import Editor from '../../Editor'
|
||||||
import { edgeAttrs, conditionNodeItemHeight, nodeWidth, portItemArgsY, conditionNodePortItemArgsY, conditionNodeHeight } from '../../../constant'
|
import { edgeAttrs, nodeWidth } from '../../../constant'
|
||||||
import RbButton from '@/components/RbButton';
|
import RbButton from '@/components/RbButton';
|
||||||
import RadioGroupBtn from '../RadioGroupBtn'
|
import RadioGroupBtn from '../RadioGroupBtn'
|
||||||
|
import { calcConditionNodeTotalHeight, getConditionNodeCasePortY } from '../../../utils'
|
||||||
|
|
||||||
interface CaseListProps {
|
interface CaseListProps {
|
||||||
value?: Array<{ logical_operator: 'and' | 'or'; expressions: { left: string; operator: string; right: string; input_type?: string; }[] }>;
|
value?: Array<{ logical_operator: 'and' | 'or'; expressions: { left: string; operator: string; right: string; input_type?: string; }[] }>;
|
||||||
@@ -60,6 +61,16 @@ const CaseList: FC<CaseListProps> = ({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const form = Form.useFormInstance();
|
const form = Form.useFormInstance();
|
||||||
|
|
||||||
|
// Recalculate node height and port Y positions without rebuilding ports
|
||||||
|
const updateNodeLayout = (cases: any[]) => {
|
||||||
|
if (!selectedNode || !graphRef?.current) return;
|
||||||
|
selectedNode.prop('size', { width: nodeWidth, height: calcConditionNodeTotalHeight(cases) });
|
||||||
|
cases.forEach((_c: any, i: number) => {
|
||||||
|
selectedNode.portProp(`CASE${i + 1}`, 'args/y', getConditionNodeCasePortY(cases, i));
|
||||||
|
});
|
||||||
|
selectedNode.portProp(`CASE${cases.length + 1}`, 'args/y', getConditionNodeCasePortY(cases, cases.length));
|
||||||
|
};
|
||||||
|
|
||||||
// Update node ports based on case count changes (add/remove cases)
|
// Update node ports based on case count changes (add/remove cases)
|
||||||
const updateNodePorts = (caseCount: number, removedCaseIndex?: number) => {
|
const updateNodePorts = (caseCount: number, removedCaseIndex?: number) => {
|
||||||
if (!selectedNode || !graphRef?.current) return;
|
if (!selectedNode || !graphRef?.current) return;
|
||||||
@@ -89,19 +100,10 @@ const CaseList: FC<CaseListProps> = ({
|
|||||||
selectedNode.removePort(port.id);
|
selectedNode.removePort(port.id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Calculate new node height: base height 88px + 30px for each additional port
|
|
||||||
const totalPorts = caseCount + 1; // IF/ELIF + ELSE
|
|
||||||
const newHeight = conditionNodeHeight + (totalPorts - 2) * conditionNodeItemHeight;
|
|
||||||
|
|
||||||
selectedNode.prop('size', { width: nodeWidth, height: newHeight })
|
const cases = form.getFieldValue(name) || [];
|
||||||
|
selectedNode.prop('size', { width: nodeWidth, height: calcConditionNodeTotalHeight(cases) });
|
||||||
|
|
||||||
// Update right port x position
|
|
||||||
currentPorts.forEach((port: any) => {
|
|
||||||
if (port.group === 'right' && port.args) {
|
|
||||||
selectedNode.portProp(port.id!, 'args/x', nodeWidth);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Add ELIF ports
|
// Add ELIF ports
|
||||||
for (let i = 0; i < caseCount; i++) {
|
for (let i = 0; i < caseCount; i++) {
|
||||||
selectedNode.addPort({
|
selectedNode.addPort({
|
||||||
@@ -109,7 +111,7 @@ const CaseList: FC<CaseListProps> = ({
|
|||||||
group: 'right',
|
group: 'right',
|
||||||
args: {
|
args: {
|
||||||
x: nodeWidth,
|
x: nodeWidth,
|
||||||
y: portItemArgsY * i + conditionNodePortItemArgsY,
|
y: getConditionNodeCasePortY(cases, i),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -120,7 +122,7 @@ const CaseList: FC<CaseListProps> = ({
|
|||||||
group: 'right',
|
group: 'right',
|
||||||
args: {
|
args: {
|
||||||
x: nodeWidth,
|
x: nodeWidth,
|
||||||
y: portItemArgsY * caseCount + conditionNodePortItemArgsY,
|
y: getConditionNodeCasePortY(cases, caseCount),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -351,7 +353,10 @@ const CaseList: FC<CaseListProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/deleteBg.svg')] rb:hover:bg-[url('@/assets/images/workflow/deleteBg_hover.svg')]"
|
className="rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/deleteBg.svg')] rb:hover:bg-[url('@/assets/images/workflow/deleteBg_hover.svg')]"
|
||||||
onClick={() => removeCondition(conditionField.name)}
|
onClick={() => {
|
||||||
|
removeCondition(conditionField.name);
|
||||||
|
setTimeout(() => updateNodeLayout(form.getFieldValue(name) || []), 100);
|
||||||
|
}}
|
||||||
></div>
|
></div>
|
||||||
</Flex>
|
</Flex>
|
||||||
)
|
)
|
||||||
@@ -360,14 +365,17 @@ const CaseList: FC<CaseListProps> = ({
|
|||||||
<Row>
|
<Row>
|
||||||
<Col flex="1">
|
<Col flex="1">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => addCondition({})}
|
onClick={() => {
|
||||||
|
addCondition({});
|
||||||
|
setTimeout(() => updateNodeLayout(form.getFieldValue(name) || []), 100);
|
||||||
|
}}
|
||||||
className={clsx("rb:py-0! rb:px-1! rb:h-4.5! rb:rounded-sm! rb:text-[12px]!")}
|
className={clsx("rb:py-0! rb:px-1! rb:h-4.5! rb:rounded-sm! rb:text-[12px]!")}
|
||||||
size="small"
|
size="small"
|
||||||
>
|
>
|
||||||
+ {t('workflow.config.addCase')}
|
+ {t('workflow.config.addCase')}
|
||||||
</Button>
|
</Button>
|
||||||
</Col>
|
</Col>
|
||||||
<Col flex="70px">
|
{caseFields.length > 1 && <Col flex="70px">
|
||||||
<RbButton
|
<RbButton
|
||||||
danger
|
danger
|
||||||
className="rb:group rb:mr-5 rb:py-0! rb:px-1! rb:h-4.5! rb:rounded-sm! rb:text-[12px]! rb:gap-0!"
|
className="rb:group rb:mr-5 rb:py-0! rb:px-1! rb:h-4.5! rb:rounded-sm! rb:text-[12px]! rb:gap-0!"
|
||||||
@@ -376,7 +384,7 @@ const CaseList: FC<CaseListProps> = ({
|
|||||||
>
|
>
|
||||||
{t('common.remove')}
|
{t('common.remove')}
|
||||||
</RbButton>
|
</RbButton>
|
||||||
</Col>
|
</Col>}
|
||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-03 15:17:48
|
* @Date: 2026-02-03 15:17:48
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-03-20 11:26:43
|
* @Last Modified time: 2026-03-24 15:01:52
|
||||||
*/
|
*/
|
||||||
import { useRef, useEffect, useState } from 'react';
|
import { useRef, useEffect, useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
@@ -17,6 +17,7 @@ import type { WorkflowConfig, NodeProperties, ChatVariable } from '../types';
|
|||||||
import { getWorkflowConfig, saveWorkflowConfig } from '@/api/application'
|
import { getWorkflowConfig, saveWorkflowConfig } from '@/api/application'
|
||||||
import { useUser } from '@/store/user';
|
import { useUser } from '@/store/user';
|
||||||
import type { FeaturesConfigForm } from '@/views/ApplicationConfig/types'
|
import type { FeaturesConfigForm } from '@/views/ApplicationConfig/types'
|
||||||
|
import { calcConditionNodeTotalHeight, getConditionNodeCasePortY } from '../utils'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Props for useWorkflowGraph hook
|
* Props for useWorkflowGraph hook
|
||||||
@@ -218,7 +219,6 @@ export const useWorkflowGraph = ({
|
|||||||
// Generate ports dynamically for if-else node based on cases
|
// Generate ports dynamically for if-else node based on cases
|
||||||
if (type === 'if-else' && config.cases && Array.isArray(config.cases)) {
|
if (type === 'if-else' && config.cases && Array.isArray(config.cases)) {
|
||||||
const totalPorts = config.cases.length + 1; // IF/ELIF + ELSE
|
const totalPorts = config.cases.length + 1; // IF/ELIF + ELSE
|
||||||
const newHeight = conditionNodeHeight + (totalPorts - 2) * conditionNodeItemHeight;
|
|
||||||
|
|
||||||
const portItems: PortMetadata[] = [
|
const portItems: PortMetadata[] = [
|
||||||
defaultPortItems[0],
|
defaultPortItems[0],
|
||||||
@@ -230,7 +230,7 @@ export const useWorkflowGraph = ({
|
|||||||
id: `CASE${i + 1}`,
|
id: `CASE${i + 1}`,
|
||||||
args: {
|
args: {
|
||||||
x: nodeWidth,
|
x: nodeWidth,
|
||||||
y: portItemArgsY * i + conditionNodePortItemArgsY,
|
y: getConditionNodeCasePortY(config.cases, i),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -240,7 +240,7 @@ export const useWorkflowGraph = ({
|
|||||||
items: portItems
|
items: portItems
|
||||||
};
|
};
|
||||||
|
|
||||||
nodeConfig.height = newHeight;
|
nodeConfig.height = calcConditionNodeTotalHeight(config.cases);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate ports dynamically for question-classifier node based on categories
|
// Generate ports dynamically for question-classifier node based on categories
|
||||||
|
|||||||
90
web/src/views/Workflow/utils.ts
Normal file
90
web/src/views/Workflow/utils.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ZhaoYing
|
||||||
|
* @Date: 2026-03-24 15:07:49
|
||||||
|
* @Last Modified by: ZhaoYing
|
||||||
|
* @Last Modified time: 2026-03-24 15:07:49
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { portItemArgsY, conditionNodePortItemArgsY, conditionNodeHeight } from './constant'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the total height of a condition (if-else) node based on its cases.
|
||||||
|
*
|
||||||
|
* The height is composed of:
|
||||||
|
* - `conditionNodeHeight`: the base height of the node (header + padding).
|
||||||
|
* - `(cases.length - 1) * 26`: vertical spacing added for each additional case
|
||||||
|
* beyond the first (each case separator row is 26px).
|
||||||
|
* - `exprCount * 20`: each individual expression row occupies 20px.
|
||||||
|
* - `hasMultiExprCount * 3`: a small extra padding (3px per expression) is added
|
||||||
|
* for cases that contain more than one expression, to account for the logical
|
||||||
|
* operator indicator (AND/OR) between expressions.
|
||||||
|
*
|
||||||
|
* @param cases - Array of case objects, each containing an `expressions` array.
|
||||||
|
* @returns The total pixel height for the condition node.
|
||||||
|
*/
|
||||||
|
export const calcConditionNodeTotalHeight = (cases: any[]) => {
|
||||||
|
// Total number of expressions across all cases
|
||||||
|
const exprCount = cases.reduce((acc: number, c: any) => acc + (c?.expressions?.length || 0), 0);
|
||||||
|
// Sum of expression counts only for cases that have more than one expression
|
||||||
|
const hasMultiExprCount = cases.reduce((acc: number, c: any) => acc + (c?.expressions?.length > 1 ? c?.expressions?.length : 0), 0);
|
||||||
|
|
||||||
|
return conditionNodeHeight + (cases.length - 1) * 26 + exprCount * 20 + hasMultiExprCount * 3;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the Y-coordinate of the right-side output port for a specific case
|
||||||
|
* in a condition (if-else) node.
|
||||||
|
*
|
||||||
|
* The port position is determined by iterating through all preceding cases
|
||||||
|
* (index 0 to caseIndex - 1) and accumulating their visual heights. Several
|
||||||
|
* pixel-level corrections are applied to align ports with the rendered UI:
|
||||||
|
*
|
||||||
|
* 1. **Base offset**: starts at `conditionNodePortItemArgsY`, which is the Y
|
||||||
|
* position of the first case port relative to the node top.
|
||||||
|
*
|
||||||
|
* 2. **Per-case accumulation**: for each preceding case with `n` expressions,
|
||||||
|
* add `portItemArgsY * (n + 1)` — this accounts for `n` expression rows
|
||||||
|
* plus one case header/separator row.
|
||||||
|
*
|
||||||
|
* 3. **Single-expression correction**: cases with exactly 1 expression render
|
||||||
|
* slightly shorter than the generic formula predicts. Subtract
|
||||||
|
* `singleExprCount * 7 + 2` to compensate for the reduced row height when
|
||||||
|
* no logical operator row is shown.
|
||||||
|
*
|
||||||
|
* 4. **Multi-expression correction**: cases with 2+ expressions have a compact
|
||||||
|
* logical operator row. Subtract `multiExprCount * 9` to offset the
|
||||||
|
* over-estimated spacing.
|
||||||
|
*
|
||||||
|
* 5. **Extra expression correction**: for cases with more than 2 expressions,
|
||||||
|
* each additional expression beyond the second introduces a minor spacing
|
||||||
|
* discrepancy. Subtract `(extraExprs + 1) * 2` to fine-tune alignment.
|
||||||
|
*
|
||||||
|
* @param cases - Array of case objects, each containing an `expressions` array.
|
||||||
|
* @param caseIndex - The zero-based index of the target case whose port Y is needed.
|
||||||
|
* @returns The Y-coordinate (in pixels) for the output port of the given case.
|
||||||
|
*/
|
||||||
|
export const getConditionNodeCasePortY = (cases: any[], caseIndex: number) => {
|
||||||
|
let y = conditionNodePortItemArgsY;
|
||||||
|
let singleExprCount = 0;
|
||||||
|
let multiExprCount = 0;
|
||||||
|
let extraExprs = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < caseIndex; i++) {
|
||||||
|
const n = cases[i]?.expressions?.length || 0;
|
||||||
|
y += portItemArgsY * (n + 1);
|
||||||
|
if (n === 1) singleExprCount++;
|
||||||
|
else if (n >= 2) {
|
||||||
|
multiExprCount++;
|
||||||
|
if (n > 2) extraExprs += n - 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Correction for single-expression cases (slightly shorter rendered height)
|
||||||
|
if (singleExprCount > 0) y -= singleExprCount * 7 + 2;
|
||||||
|
// Correction for multi-expression cases (compact logical operator row)
|
||||||
|
y -= multiExprCount * 9;
|
||||||
|
// Correction for cases with more than 2 expressions (minor spacing drift)
|
||||||
|
if (extraExprs > 0) y -= (extraExprs + 1) * 2;
|
||||||
|
|
||||||
|
return y;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user