fix(ladder): handle-branch corruption on add/delete/duplicate + QU/QD XML - #893
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (7)
🚧 Files skipped from review as they are similar to previous changes (7)
WalkthroughUpdates ladder parallel-branch handling across load, serial insertion, rung duplication, and XML export by rebuilding runtime branch state, changing predecessor lookup to id-based traversal, preserving duplicated branch context, and deriving parallel formal parameters from the actual incoming edge. ChangesLadder Parallel Branch Correctness
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (2)
src/frontend/store/slices/ladder/utils/index.ts (1)
5-5: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valuePrefer the
@root/*path alias over deep relative imports.The new import uses a deep relative path; the repo standard is the
@root/*alias for./src/*.♻️ Suggested change
-import { updateDiagramElementsPosition } from '../../../../components/_molecules/graphical-editor/ladder/rung/ladder-utils/elements/diagram' +import { updateDiagramElementsPosition } from '`@root/components/_molecules/graphical-editor/ladder/rung/ladder-utils/elements/diagram`'As per coding guidelines: "Use path alias
@root/*to reference./src/*".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/frontend/store/slices/ladder/utils/index.ts` at line 5, The import in the ladder utils module uses a deep relative path instead of the repo-standard `@root/`* alias. Update the import in index.ts to reference updateDiagramElementsPosition through the `@root` alias so it points to the same module under src/* without relative traversal.Source: Coding guidelines
src/frontend/components/_molecules/graphical-editor/ladder/rung/ladder-utils/elements/handle-branch/index.ts (1)
1342-1359: 🎯 Functional Correctness | 🔵 TrivialUse
branchContext.directiondirectly
branchContext.directionis already persisted on each branch node, so this reconstruction can use it instead of inferring direction fromoutputHandles. That removes an unnecessary dependency on block handle metadata.♻️ Use the persisted direction
- const seen = new Map<string, { blockId: string; handleId: string }>() + const seen = new Map<string, { blockId: string; handleId: string; direction: 'input' | 'output' }>() for (const node of rung.nodes) { const ctx = (node.data as BasicNodeData).branchContext if (ctx?.blockId && ctx?.handleId) { - seen.set(`${ctx.blockId}::${ctx.handleId}`, { blockId: ctx.blockId, handleId: ctx.handleId }) + seen.set(`${ctx.blockId}::${ctx.handleId}`, { + blockId: ctx.blockId, + handleId: ctx.handleId, + direction: ctx.direction, + }) } } const branches: HandleBranch[] = [] - for (const { blockId, handleId } of seen.values()) { + for (const { blockId, handleId, direction } of seen.values()) { const block = rung.nodes.find((n) => n.id === blockId) if (!block) continue - const data = block.data as BasicNodeData - const isOutput = (data.outputHandles ?? []).some((h) => h.id === handleId) - const direction: 'input' | 'output' = isOutput ? 'output' : 'input' const nodeIds = reconcileBranchNodeIds(rung, { blockId, handleId, direction, nodeIds: [] }) if (nodeIds.length > 0) branches.push({ blockId, handleId, direction, nodeIds }) }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/frontend/components/_molecules/graphical-editor/ladder/rung/ladder-utils/elements/handle-branch/index.ts` around lines 1342 - 1359, The branch direction reconstruction in deriveHandleBranches is inferring input/output from block outputHandles instead of using the persisted branchContext.direction on each node. Update the logic that builds the seen map and branches to carry branchContext.direction through, then pass that saved direction into reconcileBranchNodeIds and branch creation so the code no longer depends on BasicNodeData.outputHandles.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@src/frontend/components/_molecules/graphical-editor/ladder/rung/ladder-utils/elements/serial/index.ts`:
- Around line 65-67: The predecessor lookup in serial element insertion can
still resolve to undefined, which later causes the connect logic to dereference
previousNode.id. In the serial ladder utilities around getPreviousElement and
the connectNodes flow, add a short-circuit guard immediately after resolving
previousNode so the function exits or skips linking when no predecessor is
found, and keep the existing path unchanged when a valid node is returned.
In
`@src/frontend/components/_molecules/graphical-editor/ladder/rung/ladder-utils/elements/utils/index.ts`:
- Around line 89-97: getPreviousElement currently assumes a predecessor always
exists, but it can return undefined when newElementId is missing or is the first
serial node, while appendSerialConnection dereferences previousNode.id
unconditionally. Update getPreviousElement in the ladder utils index to return
Node | undefined, then add a guard in appendSerialConnection before calling
connectNodes so the fallback path handles the missing predecessor safely.
In `@src/frontend/store/slices/ladder/utils/index.ts`:
- Around line 223-231: The duplicate-rung flow in ladder/utils/index.ts needs
test coverage for the branch-specific remap and relayout behavior. Update
src/frontend/store/__tests__/ladder-slice.test.ts to add a case for
duplicateRung where branchContext.blockId is remapped correctly, and another
case that exercises the post-duplication updateDiagramElementsPosition pass so a
branch-expanded block re-expands and its nodes/edges are repositioned against
the duplicated rung’s actual node set.
In `@src/frontend/utils/PLC/xml-generator/codesys/language/ladder-xml.ts`:
- Around line 101-104: The formal parameter resolution in
formalParameterFromParallel is ambiguous because it only matches on srcNode.id
plus membership in the parallel chain, so duplicate edges from the same source
can serialize to the wrong pin. Update the parallel traversal in ladder-xml.ts
to carry the конкретный selected edge/sourceHandle (or equivalent target-handle
context) into the nodes.map entries and use that instead of a broad
rungEdges.find lookup. Make the same disambiguation wherever this helper is
used, and add a regression test covering a block feeding multiple branches in
the same parallel chain.
---
Nitpick comments:
In
`@src/frontend/components/_molecules/graphical-editor/ladder/rung/ladder-utils/elements/handle-branch/index.ts`:
- Around line 1342-1359: The branch direction reconstruction in
deriveHandleBranches is inferring input/output from block outputHandles instead
of using the persisted branchContext.direction on each node. Update the logic
that builds the seen map and branches to carry branchContext.direction through,
then pass that saved direction into reconcileBranchNodeIds and branch creation
so the code no longer depends on BasicNodeData.outputHandles.
In `@src/frontend/store/slices/ladder/utils/index.ts`:
- Line 5: The import in the ladder utils module uses a deep relative path
instead of the repo-standard `@root/`* alias. Update the import in index.ts to
reference updateDiagramElementsPosition through the `@root` alias so it points to
the same module under src/* without relative traversal.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: f5ac2cca-f694-4e4a-8d0a-734c578db228
📒 Files selected for processing (7)
src/frontend/components/_molecules/graphical-editor/ladder/rung/ladder-utils/elements/handle-branch/index.tssrc/frontend/components/_molecules/graphical-editor/ladder/rung/ladder-utils/elements/serial/index.tssrc/frontend/components/_molecules/graphical-editor/ladder/rung/ladder-utils/elements/utils/__tests__/get-previous-element.test.tssrc/frontend/components/_molecules/graphical-editor/ladder/rung/ladder-utils/elements/utils/index.tssrc/frontend/store/slices/ladder/slice.tssrc/frontend/store/slices/ladder/utils/index.tssrc/frontend/utils/PLC/xml-generator/codesys/language/ladder-xml.ts
| if (!previousNode) { | ||
| previousNode = getPreviousElement( | ||
| { ...rung, nodes: newNodes, edges: newEdges }, | ||
| newNodes.findIndex((n) => n.id === newElement.id), | ||
| ) | ||
| previousNode = getPreviousElement({ ...rung, nodes: newNodes, edges: newEdges }, newElement.id) | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Guard against an undefined predecessor here.
getPreviousElement can return undefined (see root-cause note in utils/index.ts), after which previousNode.id at Line 93 would throw. A short-circuit before the connectNodes calls keeps this path safe.
🛡️ Bail out when no predecessor is resolved
if (!previousNode) {
previousNode = getPreviousElement({ ...rung, nodes: newNodes, edges: newEdges }, newElement.id)
}
+ if (!previousNode) return { nodes: newNodes, edges: newEdges }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (!previousNode) { | |
| previousNode = getPreviousElement( | |
| { ...rung, nodes: newNodes, edges: newEdges }, | |
| newNodes.findIndex((n) => n.id === newElement.id), | |
| ) | |
| previousNode = getPreviousElement({ ...rung, nodes: newNodes, edges: newEdges }, newElement.id) | |
| } | |
| if (!previousNode) { | |
| previousNode = getPreviousElement({ ...rung, nodes: newNodes, edges: newEdges }, newElement.id) | |
| } | |
| if (!previousNode) return { nodes: newNodes, edges: newEdges } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@src/frontend/components/_molecules/graphical-editor/ladder/rung/ladder-utils/elements/serial/index.ts`
around lines 65 - 67, The predecessor lookup in serial element insertion can
still resolve to undefined, which later causes the connect logic to dereference
previousNode.id. In the serial ladder utilities around getPreviousElement and
the connectNodes flow, add a short-circuit guard immediately after resolving
previousNode so the function exits or skips linking when no predecessor is
found, and keep the existing path unchanged when a valid node is returned.
| export const getPreviousElement = (rung: RungLadderState, newElementId: string): Node => { | ||
| const serialSpine = rung.nodes.filter( | ||
| (n) => | ||
| n.type !== 'placeholder' && | ||
| n.type !== 'parallelPlaceholder' && | ||
| n.type !== 'variable' && | ||
| !(n.data as BasicNodeData).branchContext, | ||
| ) | ||
| return nodesWithNoPlaceholderAndVariables[nodeIndex - 1] | ||
| return serialSpine[serialSpine.findIndex((n) => n.id === newElementId) - 1] |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -nP -C4 'getPreviousElement\s*\(' src/frontendRepository: Autonomy-Logic/openplc-editor
Length of output: 5467
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '1,140p' src/frontend/components/_molecules/graphical-editor/ladder/rung/ladder-utils/elements/utils/index.ts
printf '\n---\n'
sed -n '1,180p' src/frontend/components/_molecules/graphical-editor/ladder/rung/ladder-utils/elements/serial/index.ts
printf '\n---\n'
sed -n '1,180p' src/frontend/components/_molecules/graphical-editor/ladder/rung/ladder-utils/elements/utils/__tests__/get-previous-element.test.ts
EOFRepository: Autonomy-Logic/openplc-editor
Length of output: 12013
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '1,140p' src/frontend/components/_molecules/graphical-editor/ladder/rung/ladder-utils/elements/utils/index.ts
printf '\n---\n'
sed -n '1,180p' src/frontend/components/_molecules/graphical-editor/ladder/rung/ladder-utils/elements/serial/index.ts
printf '\n---\n'
sed -n '1,180p' src/frontend/components/_molecules/graphical-editor/ladder/rung/ladder-utils/elements/utils/__tests__/get-previous-element.test.tsRepository: Autonomy-Logic/openplc-editor
Length of output: 11972
Guard the predecessor lookup before dereferencing
getPreviousElement() can return undefined when the id is missing or is the first serial node, but it is typed as Node. appendSerialConnection() then calls previousNode.id without a guard, which can throw. Return Node | undefined and handle the fallback before connectNodes().
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@src/frontend/components/_molecules/graphical-editor/ladder/rung/ladder-utils/elements/utils/index.ts`
around lines 89 - 97, getPreviousElement currently assumes a predecessor always
exists, but it can return undefined when newElementId is missing or is the first
serial node, while appendSerialConnection dereferences previousNode.id
unconditionally. Update getPreviousElement in the ladder utils index to return
Node | undefined, then add a guard in appendSerialConnection before calling
connectNodes so the fallback path handles the missing predecessor safely.
495e49c to
be36234
Compare
|
Rebased onto the revert branch |
be36234 to
e6817da
Compare
…cate and XML Four related handle-branch defects in the ladder editor: - Adding an element to a block's primary output (e.g. a coil on a counter's Q/QU) spliced it into a secondary-handle branch edge, dropping the element and breaking the branch (the originally reported "coil doesn't show on Q, contact-to-R connection breaks"). The serial predecessor was resolved by array index, which picks the handle-branch contact interleaved between the block and the right rail. getPreviousElement now skips branchContext nodes (mirroring getPreviousElementsByEdge) and is keyed by element id. - handleBranches is runtime-only state (not persisted in .ld); rebuild it from the graph on project load via deriveHandleBranches so the first branch-aware edit no longer corrupts diagrams that contain handle branches. - Duplicating a rung rebuilt blocks at default dimensions without re-running the layout solver, and dropped branchContext remapping on branch parallel nodes; re-run updateDiagramElementsPosition and remap. - PLCopen XML serialized every block-output branch to the primary output pin (QU); resolve the actual edge sourceHandle so QU/QD are distinguished. Adds a regression test for getPreviousElement. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
e6817da to
9f46101
Compare
Summary
Fixes four related handle-branch defects in the ladder editor. The headline bug: adding a coil to a counter block's primary output (e.g.
Q/QU) corrupted an existing input-handle branch — the coil didn't appear on the output and the branch contact's connection to the block was severed.Root causes & fixes
Add-to-primary-output corrupts a handle branch (
elements/serial,elements/utils)The serial predecessor for a main-line insert was resolved by array index (
getPreviousElement). Handle-branch elements (e.g. a contact wired to a counter'sRinput) are interleaved in the node array between the block and the right rail, so a coil dropped on the output picked the branch contact as its predecessor and was spliced into the branch edge — dropping the coil and breaking the branch.getPreviousElementnow skipsbranchContextnodes (mirroringgetPreviousElementsByEdge) and is keyed by element id. Integrated with the existingedgeSourceIdkeyboard-selection path.handleBranches lost on load (
store/slices/ladder/slice)handleBranchesis runtime-only state (not persisted in.ld). It's now rebuilt from the graph on project load viaderiveHandleBranches, so the first branch-aware edit no longer corrupts diagrams that contain handle branches.Duplicate rung corrupts branched blocks (
store/slices/ladder/utils)Duplication rebuilt blocks at default dimensions without re-running the layout solver and dropped
branchContextremapping on branch parallel nodes. Now re-runsupdateDiagramElementsPositionand remaps the parallelbranchContext.PLCopen XML maps all block-output branches to the primary pin (
ladder-xml)Every block-output branch was serialized as connected to the primary output (
QU). The serializer now resolves the actual edgesourceHandle, distinguishingQU/QD.Testing
getPreviousElement(branch-aware predecessor).ladder-slice,ladder-xml) — 165 tests under jest.Rbranch stays intact, no orphan edges (reproduced before/after).Companion PR
Mirrored byte-identical to the shared surface in openplc-web: Autonomy-Logic/openplc-web#546 (same commit).
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Tests