Skip to content

Conversation

@priyanshu6238
Copy link
Collaborator

@priyanshu6238 priyanshu6238 commented Jan 8, 2026

Summary by CodeRabbit

Release Notes

  • New Features

    • Added copy functionality for nodes and actions. Users can now duplicate nodes and actions in the flow editor; copy buttons appear on hover within node and action headers.
  • Style

    • Added styling for the new copy button control with hover visibility effects.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 8, 2026

📝 Walkthrough

Walkthrough

The changes introduce a node copy feature to the flow editor. The implementation adds copy button UI components to the TitleBar, wires copy handlers in the Action and Node components, and introduces a copyNode thunk that deep-copies nodes with UUID remapping for all sub-structures while managing store state and positioning.

Changes

Cohort / File(s) Summary
UI Components
src/components/titlebar/TitleBar.tsx, src/components/titlebar/TitleBar.module.scss
Added copy button UI with public props onCopy and showCopy. TitleBar renders a copy button when showCopy is true and config is mutable. Styling includes .copy_button class with opacity transition on hover; removed right: 0 from .remove_button.
Flow Components
src/components/flow/actions/action/Action.tsx, src/components/flow/node/Node.tsx
Both components import copyNode thunk and wire it to their respective UI layers. Each adds handleCopy method to invoke copyNode with current node/action, and exposes showCopy and onCopy props to TitleBar. Dispatch mappings updated to include copyNode thunk.
Store Logic
src/store/thunks.ts
Introduces copyNode thunk and CopyNode type. Thunk performs deep copy of node with new UUIDs generated for node, actions, exits, and router components (categories, cases, defaults). Remaps UUID references, nullifies exit destinations, offsets position, resets inbound connections, and removes ghost flag. Merges result into node map and marks state dirty.

Sequence Diagram

sequenceDiagram
    actor User
    participant Component as Action/Node<br/>Component
    participant TitleBar
    participant Store as Store/<br/>Thunk
    participant NodeMap as Node Map

    User->>TitleBar: Clicks copy button
    TitleBar->>Component: onCopy callback triggered
    Component->>Store: Dispatches copyNode(renderNode)
    Store->>Store: Generates new UUIDs for:<br/>- Node<br/>- Actions<br/>- Exits<br/>- Router components
    Store->>Store: Remaps UUID references<br/>Nullifies destinations<br/>Offsets position
    Store->>NodeMap: Merges copied node<br/>into node map
    Store->>Store: Marks state as dirty
    Store-->>Component: Returns updated RenderNodeMap
    Component-->>User: Display shows copied node
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 A flutter of whiskers, a tap on the clone!
I've copied your nodes to a place of their own,
With UUIDs fresh and positions askew,
Now your flow has two paths where once there was true!
The rabbit's great gift—duplication's delight! 🎉

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title 'Copy functionality for nodes and actions' directly and accurately summarizes the main change in the PR, which adds copy capability across multiple components (Node, Action, TitleBar) and the store layer.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@priyanshu6238 priyanshu6238 self-assigned this Jan 8, 2026
@priyanshu6238 priyanshu6238 linked an issue Jan 8, 2026 that may be closed by this pull request
@priyanshu6238 priyanshu6238 changed the title Feat/copy node Copy functionality for nodes and actions Jan 8, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @src/store/thunks.ts:
- Around line 994-1024: The code currently casts clonedNode.node.router to
SwitchRouter unconditionally which is imprecise; change the typing to the base
Router (or use a type guard) and only treat it as a SwitchRouter when confirmed:
replace the unconditional cast of clonedNode.node.router as SwitchRouter with
either casting to Router or, better, guard with if (router.type ===
RouterTypes.switch) before accessing SwitchRouter-specific fields (router.cases,
router.default_category_uuid), while keeping the existing optional checks on
router.categories to preserve runtime safety.
🧹 Nitpick comments (2)
src/components/titlebar/TitleBar.module.scss (1)

40-50: Minor inconsistency in hiding approach between copy and remove buttons.

The .copy_button uses opacity: 0 / opacity: 1 for hide/show, while .remove_button uses visibility: hidden / visibility: visible. While both work, visibility: hidden prevents mouse events on the hidden element, whereas opacity: 0 still allows clicks on the invisible button. Consider using visibility for consistency with the existing pattern, or add pointer-events: none when opacity is 0.

♻️ Optional: Use visibility for consistency
 .copy_button {
   padding-right: 5px;
   width: 20px;
   color: rgba(255, 255, 255, 0.8);
   display: flex;
   align-items: center;
   justify-content: center;
   cursor: pointer;
-  opacity: 0;
-  transition: opacity 0.2s;
+  visibility: hidden;
 }
 &:hover {
   .copy_button {
-    opacity: 1;
+    visibility: visible;
   }

Also applies to: 70-72

src/components/flow/actions/action/Action.tsx (1)

104-113: Redundant guard in handleCopy.

The if (this.props.first) check on line 110 is redundant because showCopy (line 213) already ensures the copy button is only rendered when this.props.first is true. The handler will only be called when the button is visible.

That said, keeping it as a defensive check is acceptable if you prefer explicit safety.

♻️ Optional: Remove redundant guard
 public handleCopy(event: React.MouseEvent<HTMLElement>): void {
   if (event) {
     event.preventDefault();
     event.stopPropagation();
   }
-  // Only show copy on the first action to copy the entire node
-  if (this.props.first) {
-    this.props.copyNode(this.props.renderNode);
-  }
+  this.props.copyNode(this.props.renderNode);
 }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4a7f1a7 and 6be058e.

⛔ Files ignored due to path filters (4)
  • src/components/flow/actions/action/__snapshots__/Action.test.ts.snap is excluded by !**/*.snap
  • src/components/flow/actions/action/__snapshots__/Action.test.tsx.snap is excluded by !**/*.snap
  • src/components/flow/node/__snapshots__/Node.test.tsx.snap is excluded by !**/*.snap
  • src/components/titlebar/__snapshots__/TitleBar.test.tsx.snap is excluded by !**/*.snap
📒 Files selected for processing (5)
  • src/components/flow/actions/action/Action.tsx
  • src/components/flow/node/Node.tsx
  • src/components/titlebar/TitleBar.module.scss
  • src/components/titlebar/TitleBar.tsx
  • src/store/thunks.ts
🧰 Additional context used
🧬 Code graph analysis (4)
src/store/thunks.ts (3)
src/store/flowContext.ts (4)
  • RenderNode (26-32)
  • RenderNodeMap (18-20)
  • nodes (244-251)
  • updateNodes (176-181)
src/utils/index.tsx (2)
  • createUUID (384-386)
  • NODE_SPACING (26-26)
src/flowTypes.ts (4)
  • Exit (192-195)
  • SwitchRouter (241-245)
  • Category (223-227)
  • Case (214-221)
src/components/flow/node/Node.tsx (1)
src/store/thunks.ts (1)
  • CopyNode (101-101)
src/components/flow/actions/action/Action.tsx (1)
src/store/thunks.ts (1)
  • CopyNode (101-101)
src/components/titlebar/TitleBar.tsx (1)
src/utils/index.tsx (1)
  • createClickHandler (217-234)
🔇 Additional comments (13)
src/components/titlebar/TitleBar.tsx (3)

16-17: LGTM!

The new props onCopy and showCopy follow the same optional pattern as the existing onMoveUp and showMove props.


184-184: LGTM!

The copy element is properly integrated into the render flow, positioned between the title text and the remove button, which is a logical placement for the UI.

Also applies to: 193-193


111-129: Implementation follows established patterns.

The getCopy() method correctly mirrors the structure of getMoveArrow() and getRemove(), using createClickHandler consistently and providing a placeholder element when the copy button is not shown to maintain layout consistency. The copy icon exists in the temba-icon component library.

src/components/flow/node/Node.tsx (4)

36-38: LGTM!

The imports are correctly added following the existing import pattern from store/thunks.


217-221: LGTM!

The handleCopy method correctly prevents event propagation (matching the pattern in handleRemoval) and invokes the copyNode thunk with the current render node.


382-383: LGTM!

The showCopy and onCopy props are correctly wired to the TitleBar, with showCopy following the same condition as showRemoval (disabled during translation mode).


556-556: LGTM!

The copyNode thunk is correctly added to the dispatch bindings.

src/components/flow/actions/action/Action.tsx (2)

213-214: LGTM!

The copy button is correctly shown only on the first action and when not translating, ensuring users copy the entire node from a single, consistent location.


262-263: LGTM!

The dispatch mapping and connect options are correctly updated.

Also applies to: 268-270

src/store/thunks.ts (4)

13-13: LGTM!

The imports for Case and SwitchRouter are correctly added to support the router UUID remapping logic.

Also applies to: 23-23


101-102: LGTM!

The CopyNode type is correctly defined to match the thunk signature pattern used elsewhere in this file.


956-1039: Overall implementation is solid.

The copyNode thunk correctly:

  • Deep-clones the node structure
  • Remaps all internal UUIDs (node, actions, exits, router components)
  • Maintains structural relationships (category→exit, case→category, default_category)
  • Nullifies exit destinations (copied node is disconnected)
  • Offsets position to avoid overlap
  • Clears inbound connections and ghost flag
  • Dispatches update and marks state dirty

The use of JSON.parse(JSON.stringify()) for deep cloning is acceptable here since RenderNode contains only serializable data.


1026-1029: No action needed—operand references in ui.config correctly reference external assets.

The deep clone creates a copy of the entire node structure without explicitly remapping ui.config. Since operand references (e.g., operand.id) point to external assets in the assetStore (fields, results) rather than internal node UUIDs, they should remain unchanged when cloning. This behavior is correct.

Comment on lines +994 to +1024
if (clonedNode.node.router) {
const router = clonedNode.node.router as SwitchRouter;

if (router.categories) {
router.categories.forEach((category: Category) => {
const newCategoryUUID = createUUID();
uuidMap[category.uuid] = newCategoryUUID;
category.uuid = newCategoryUUID;

if (uuidMap[category.exit_uuid]) {
category.exit_uuid = uuidMap[category.exit_uuid];
}
});
}

if (router.cases) {
router.cases.forEach((caseItem: Case) => {
const newCaseUUID = createUUID();
uuidMap[caseItem.uuid] = newCaseUUID;
caseItem.uuid = newCaseUUID;

if (uuidMap[caseItem.category_uuid]) {
caseItem.category_uuid = uuidMap[caseItem.category_uuid];
}
});
}

if (router.default_category_uuid && uuidMap[router.default_category_uuid]) {
router.default_category_uuid = uuidMap[router.default_category_uuid];
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for router interface definitions
rg -n "interface.*Router" --type ts | head -20
rg -n "type.*Router" --type ts | head -20

Repository: glific/floweditor

Length of output: 3935


🏁 Script executed:

#!/bin/bash
# Look at the specific file and surrounding context
head -1030 src/store/thunks.ts | tail -100

Repository: glific/floweditor

Length of output: 2730


🏁 Script executed:

#!/bin/bash
# Find where SwitchRouter and other routers are defined
fd . --type f --name "*.ts" | xargs rg -l "SwitchRouter|RandomRouter" | head -10

Repository: glific/floweditor

Length of output: 647


🏁 Script executed:

#!/bin/bash
# Get the Router and SwitchRouter interface definitions
sed -n '202,300p' src/flowTypes.ts

Repository: glific/floweditor

Length of output: 1661


🏁 Script executed:

#!/bin/bash
# Search for all router type definitions
rg -n "Router.*extends|Router.*{" src/flowTypes.ts -A 5 | head -60

Repository: glific/floweditor

Length of output: 174


🏁 Script executed:

#!/bin/bash
# Check if RandomRouter is defined and what properties it has
rg -n "RandomRouter" src/flowTypes.ts -B 2 -A 10

Repository: glific/floweditor

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Look for RandomRouter definition and check all router types
rg -n "interface RandomRouter|type RandomRouter" src/flowTypes.ts -A 10

Repository: glific/floweditor

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Check what router types exist in RouterTypes enum
rg -n "enum RouterTypes" src/flowTypes.ts -A 20

Repository: glific/floweditor

Length of output: 475


🏁 Script executed:

#!/bin/bash
# Check how copyNode function is used and what router types can be passed
rg -n "copyNode" src/store/thunks.ts -B 5 -A 5

Repository: glific/floweditor

Length of output: 390


The type casting approach is safe at runtime but should be reconsidered for type safety.

The codebase only has two router types: switch and random. Since RandomRouter has no specific interface definition, it inherits only the base Router interface which includes categories as a required property. The SwitchRouter-specific properties (cases, default_category_uuid) are safely guarded by optional checks before access, so the code won't crash on non-SwitchRouter types. However, casting to SwitchRouter unconditionally is imprecise—consider casting to the Router base type instead or using a type guard (if (router.type === RouterTypes.switch)) for better type safety and clarity.

🤖 Prompt for AI Agents
In @src/store/thunks.ts around lines 994 - 1024, The code currently casts
clonedNode.node.router to SwitchRouter unconditionally which is imprecise;
change the typing to the base Router (or use a type guard) and only treat it as
a SwitchRouter when confirmed: replace the unconditional cast of
clonedNode.node.router as SwitchRouter with either casting to Router or, better,
guard with if (router.type === RouterTypes.switch) before accessing
SwitchRouter-specific fields (router.cases, router.default_category_uuid), while
keeping the existing optional checks on router.categories to preserve runtime
safety.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[To be explored] Copy-Paste Functionality for Nodes

2 participants