Skip to content

Conversation

@KCSAbeywickrama
Copy link
Contributor

@KCSAbeywickrama KCSAbeywickrama commented Nov 19, 2025

Purpose

Resolve wso2/product-ballerina-integrator#1664

Summary by CodeRabbit

  • New Features

    • Added JOIN clause type and ArrayJoin mapping support
    • Clause connector UI and visualization added to the data mapper diagram
  • Improvements

    • Query clause editor now supports left/right expression fields and outer-join option
    • Mappings can use multiple source fields and better track input-focus for queries
    • Side panel and clause workflow: automatic clause add behavior and tighter clause-panel integration

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

…eryExprLabelWidget references and clean up QueryOutputNode by removing unused query handling code
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 19, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds JOIN/ArrayJoin support across the data mapper: core interface updates for focus tracking and clause props; new ClauseConnector node (model/factory/widget); array expansion and mapping changes; UI clause editor/store integration; Query traversal and visitor hooks; QueryExprLabelWidget removed.

Changes

Cohort / File(s) Summary
Core interfaces
workspaces/ballerina/ballerina-core/src/interfaces/data-mapper.ts
Added JOIN = "join" to IntermediateClauseType; removed focusedMemberId from IOType; added focusInputRootMap?: Record<string,string> and traversingRoot?: string; added lhsExpression, rhsExpression, isOuter to IntermediateClauseProps.
RPC / model expansion
workspaces/ballerina/ballerina-extension/src/rpc-managers/data-mapper/utils.ts
expandDMModel and input processing initialize/track traversingRoot and populate focusInputRootMap; ExpandedDMModel return includes focusInputRootMap.
Visualizer signature
workspaces/ballerina/ballerina-visualizer/src/views/DataMapper/DataMapperView.tsx
ModelSignature.inputs now concatenates model.inputs with model.query inputs when present.
DataMapper init / context
workspaces/ballerina/data-mapper/src/components/DataMapper/DataMapperEditor.tsx, workspaces/ballerina/data-mapper/src/utils/DataMapperContext/DataMapperContext.ts
Explicit initialView: View[] typing; IDataMapperContext/constructor gained hasInputsOutputsChanged property/parameter.
Clause editor & panel UI
workspaces/ballerina/data-mapper/src/components/DataMapper/SidePanel/QueryClauses/ClauseEditor.tsx, .../ClausesPanel.tsx
ClauseEditor: added JOIN option, lhs/rhs/isOuter fields, submit/cancel draft handling and store integration; ClausesPanel: listens to clauseToAdd, auto-add effect, width increased.
View model update
workspaces/ballerina/data-mapper/src/components/DataMapper/Views/DataMapperView.ts
View.sourceField?: stringsourceFields?: string[].
Diagram: new clause connector
workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/*, workspaces/ballerina/data-mapper/src/components/Diagram/Node/index.ts
New ClauseConnectorNode model, factory, widget and barrel export; registered in Node index.
Diagram engine & intermediate nodes
workspaces/ballerina/data-mapper/src/components/Diagram/Diagram.tsx, .../Actions/*, .../Actions/utils.ts, .../focus-positioning-utils.ts
Register ClauseConnectorNodeFactory; include ClauseConnectorNode in intermediate node lists, repositioning, scroll logic and target resolution.
Label / expression changes
workspaces/ballerina/data-mapper/src/components/Diagram/Label/ExpressionLabelFactory.tsx, .../ExpressionLabelModel.ts
Removed QueryExprLabelWidget path; ExpressionLabelModel lost isQuery; when pendingMappingType === MappingType.ArrayJoin render suppressed.
Removed widget
workspaces/ballerina/data-mapper/src/components/Diagram/Label/QueryExprLabelWidget.tsx
Deleted QueryExprLabelWidget component, props, and styles.
Mapping types & mapping helpers
workspaces/ballerina/data-mapper/src/components/Diagram/Link/DataMapperLink/DataMapperLink.ts, .../utils/modification-utils.ts, .../Port/model/InputOutputPortModel.ts
Added MappingType.ArrayJoin; added mapWithJoin(link) to dispatch JOIN clause and open clause panel; InputOutputPortModel invokes mapWithJoin for ArrayJoin pending mappings; expandArrayFn call sites updated to pass arrays.
Array expansion / common utils
workspaces/ballerina/data-mapper/src/components/Diagram/utils/common-utils.ts
expandArrayFn signature changed to accept inputIds: string[]; base view uses sourceFields; detection of ArrayJoin when target port name ends with ".#".
Node / port resolution utilities
workspaces/ballerina/data-mapper/src/components/Diagram/utils/node-utils.ts, .../port-utils.ts
findInputNode may return `InputNode
Input / array handling
workspaces/ballerina/data-mapper/src/components/Diagram/Node/Input/InputNode.ts, .../commons/DataMapperNode.ts
InputNode collects view.sourceFields (flatMap) for focused fields; removed resolveArrayMemberField helper; array member uses field?.member directly.
Query output / expr connector updates
workspaces/ballerina/data-mapper/src/components/Diagram/Node/QueryOutput/QueryOutputNode.ts, .../QueryExprConnector/QueryExprConnectorNodeWidget.tsx
Removed special-case query output extraction/linking; QueryExprConnector now calls expandArrayFn with full mapping.inputs.
CreateLinkState / link selection
workspaces/ballerina/data-mapper/src/components/Diagram/LinkState/CreateLinkState.ts
Removed isQueryHeaderPort guards; ClauseConnectorNode treated like LinkConnectorNode for parent/port resolution and mapping completion.
Diagram port model
workspaces/ballerina/data-mapper/src/components/Diagram/Port/model/InputOutputPortModel.ts
For ArrayJoin pending mappings, invokes mapWithJoin(lm) before returning.
Diagram hooks & traversal / visitors
workspaces/ballerina/data-mapper/src/components/Diagram/hooks/useDiagramModel.ts, .../model-utils.ts, .../visitors/*
Derived queryIOs included in queryKey; traverseNode now visits model.query via new traverseQuery; BaseVisitor adds beginVisitQuery/endVisitQuery; IntermediateNodeInitVisitor implements beginVisitQuery to create ClauseConnectorNode.
Store: query clauses panel
workspaces/ballerina/data-mapper/src/store/store.ts
DataMapperQueryClausesPanelState extended with clauseToAdd and setClauseToAdd; store init/reset updated to manage clause drafts.
Misc: diagram label/node adjustments
various
Minor imports/filters updated to include ClauseConnectorNode in intermediate node lists and utils; QueryExprLabelWidget references removed across modules.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant ClauseEditor
    participant DMStore
    participant ClausesPanel
    participant Diagram
    participant ClauseNode

    rect rgb(240,248,255)
    Note over User,ClauseEditor: Create JOIN clause via UI
    User->>ClauseEditor: choose JOIN, fill lhs/rhs, submit
    ClauseEditor->>DMStore: setClauseToAdd(newClause)
    ClauseEditor->>DMStore: clearDraft()
    DMStore-->>ClausesPanel: clauseToAdd updated
    ClausesPanel->>Diagram: apply clause to model
    end

    rect rgb(240,255,240)
    Note over Diagram,ClauseNode: Diagram creates ClauseConnectorNode
    Diagram->>Diagram: traverseNode() visits model.query
    Diagram->>ClauseNode: beginVisitQuery -> create node
    ClauseNode->>ClauseNode: initPorts()
    ClauseNode->>ClauseNode: initLinks()
    Diagram-->>User: clause connector rendered
    end
Loading
sequenceDiagram
    participant Caller
    participant expandArrayFn
    participant model

    Caller->>expandArrayFn: expandArrayFn(context, inputIds[], outputId, viewId)
    expandArrayFn->>expandArrayFn: build baseView using sourceFields = inputIds
    alt target port name ends with ".#"
        expandArrayFn->>expandArrayFn: return MappingType.ArrayJoin
    else
        expandArrayFn->>expandArrayFn: return ArrayToArray or ArrayToSingleton
    end
    expandArrayFn-->>Caller: mapping type & view
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Files/areas to pay extra attention:
    • ClauseConnectorNode (model/widget/factory) — port/link initialization and event propagation
    • expandArrayFn signature change and all call sites (sourceFields semantics)
    • node-utils fallback lookup (focusInputRootMap) and widened findInputNode return type
    • mapWithJoin flow and UI store interaction (clauseToAdd, opening clause panel)
    • Removal of QueryExprLabelWidget and associated label factory/model adjustments

Suggested reviewers

  • hevayo
  • gigara

Poem

🐰 I hopped through nodes and joined the fields,
Mapped roots with care and flipped old shields.
Ports now gather, clauses take flight,
Expressions hum in the diagram light.
A merry join — the rabbit nods: delight! ✨

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings, 2 inconclusive)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is largely incomplete, providing only the Purpose section with a link. The Goals, Approach, and most other required template sections are entirely missing. Complete the PR description by filling in Goals, Approach (with UI changes details), and other relevant sections from the template to provide full context for reviewers.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Linked Issues check ❓ Inconclusive The linked issue #1664 contains only a placeholder '$subject' for its description, providing insufficient detail to validate whether the code changes meet the stated requirements. Update the linked issue #1664 with a complete description outlining specific requirements for join clause support, then verify the PR implementation matches those requirements.
Out of Scope Changes check ❓ Inconclusive Without a complete linked issue description, it is difficult to definitively determine which changes are in-scope versus out-of-scope for the join clause feature. Clarify the scope requirements in the linked issue and review the PR changes against those requirements to identify any out-of-scope modifications.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The PR title '[BI Data Mapper] Add support for the join clause' clearly summarizes the main change - adding join clause functionality to the data mapper.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

Copy link
Contributor

@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: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
workspaces/ballerina/data-mapper/src/components/DataMapper/SidePanel/QueryClauses/ClausesPanel.tsx (1)

36-79: Effect cleanup clears clauseToAdd before consumers read it—fix store access and cleanup timing

The effect both reacts to and clears clauseToAdd on every dependency change:

useEffect(() => {
    if (clauseToAdd) {
        setAdding(clauses.length - 1);
    }
    return () => {
        setClauseToAdd(undefined);
    };
}, [clauseToAdd, clauses.length, setClauseToAdd, setAdding]);

Root cause: You're accessing clauseToAdd via .getState() (non-reactive snapshot) while depending on it in the effect. When an external trigger (e.g., mapWithJoin in modification-utils.ts) calls setClauseToAdd({type: JOIN, ...}), ClausesPanel doesn't re-render because there's no hook subscription on that state. But if clauses.length or other dependencies change during the same render cycle, the cleanup fires and resets clauseToAdd to undefined before ClauseEditor (line 37: clause ?? clauseToAdd ?? {}) can read it for pre-population.

Fix:

  1. Use hook-based access for all store state to ensure reactivity:
const {
    isQueryClausesPanelOpen,
    setIsQueryClausesPanelOpen,
    clauseToAdd,
    setClauseToAdd,
} = useDMQueryClausesPanelStore();
  1. Move cleanup responsibility to the component that consumes the value (ClauseEditor), or only reset on unmount:
useEffect(() => {
    if (clauseToAdd) {
        setAdding(clauses.length - 1);
    }
}, [clauseToAdd, clauses.length, setAdding]);

useEffect(() => {
    return () => {
        setClauseToAdd(undefined);
    };
}, [setClauseToAdd]); // Only cleanup on unmount
🧹 Nitpick comments (11)
workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNodeWidget.tsx (1)

54-58: Remove unused loadingScreen variable.

The loadingScreen element is defined but never rendered in the component. If loading state handling is planned for the future, consider adding a TODO comment; otherwise, remove this unused code.

Apply this diff to remove the unused variable:

-    const loadingScreen = (
-        <div className={classnames(classes.element, classes.loadingContainer)}>
-            <ProgressRing sx={{ height: '16px', width: '16px' }} />
-        </div>
-    );
-
     return (
workspaces/ballerina/data-mapper/src/components/Diagram/hooks/useDiagramModel.ts (1)

43-105: Include query IOs in the query key to track query-structure changes

Deriving queryIOs from model.query.inputs and model.query.output and wiring it into the useQuery key is a reasonable way to refresh the diagram when the query IO shape changes, without over‑invalidating on unrelated query edits. This looks correct as long as inputs/output are stable primitive IDs; if they ever become richer objects, consider switching to a more explicit serialization (e.g., JSON.stringify) to avoid [object Object] collisions.

workspaces/ballerina/data-mapper/src/components/Diagram/Label/ExpressionLabelFactory.tsx (1)

26-45: Skip mapping-options UI for ArrayJoin links

Using pendingMappingType to short‑circuit and render nothing for MappingType.ArrayJoin avoids showing the standard mapping options UI on JOIN links, which makes sense now that JOIN is driven via the clauses panel. You might optionally consider a small non‑interactive “JOIN” badge here for discoverability, but the current behavior is consistent with the new flow.

workspaces/ballerina/data-mapper/src/components/DataMapper/DataMapperEditor.tsx (1)

226-268: Wire hasInputsOutputsChanged into DataMapperContext; consider ClauseConnector reuse

Passing hasInputsOutputsChanged into DataMapperContext here matches the new constructor shape and lets the context react appropriately when the IO structure changes. That part looks correct.

Separately, IntermediateNodeInitVisitor is currently seeded only with existing LinkConnectorNode/QueryExprConnectorNode instances:

nodes.filter(node => node instanceof LinkConnectorNode || node instanceof QueryExprConnectorNode)

If ClauseConnectorNode instances also need their positions or other state preserved across regenerations, consider extending this filter (and the corresponding import) to include ClauseConnectorNode as well.

workspaces/ballerina/data-mapper/src/components/Diagram/Node/Input/InputNode.ts (1)

54-60: Tighten flatMap usage for sourceFields

flatMap(view => view.sourceFields).filter(Boolean) works but relies on filtering out undefined values. For clearer intent and better type inference, consider returning an array explicitly:

const focusedFieldFQNs = [
    ...this.context.views.flatMap(view => view.sourceFields ?? []),
    ...(this.context.model.query?.inputs || []),
];

This keeps focusedFieldFQNs strictly typed to string[] without relying on filter(Boolean).

workspaces/ballerina/data-mapper/src/visitors/IntermediateNodeInitVisitor.ts (1)

18-50: Query handling via ClauseConnectorNode is consistent with existing visitor pattern

The new beginVisitQuery correctly wires queries into the intermediate node set using ClauseConnectorNode, aligned with the extended BaseVisitor API. This keeps query handling parallel to mapping connector nodes. If you later need to reuse existing clause connector nodes across refreshes, you might extend findExistingNode to cover ClauseConnectorNode as well, but that’s not required for correctness now.

workspaces/ballerina/ballerina-extension/src/rpc-managers/data-mapper/utils.ts (1)

444-472: focusInputRootMap wiring and traversal context look coherent; document mutation assumptions

The new focusInputRootMap plumbing in expandDMModel, processInputRoots, and processArray is internally consistent:

  • expandDMModel returns focusInputRootMap alongside triggerRefresh, matching the ExpandedDMModel shape.
  • processInputRoots initializes model.focusInputRootMap = {} and uses a preProcessedModel with traversingRoot set per input, so processArray can track which root an array focus member originated from.
  • processArray writes model.focusInputRootMap[parentId] = model.traversingRoot only in the focus‑member branch, which is reached via that preprocessed model.

Given that processArray assumes model.focusInputRootMap is initialized, it would be good to either:

  • Add a brief comment in processInputRoots noting that it’s responsible for initializing focusInputRootMap used by downstream array processing, or
  • Defensively initialize in processArray:
if (model.traversingRoot) {
    model.focusInputRootMap ??= {};
    model.focusInputRootMap[parentId] = model.traversingRoot;
}

This protects against future internal callers that might pass a DMModel without going through processInputRoots.

Also applies to: 479-501, 592-612

workspaces/ballerina/data-mapper/src/components/DataMapper/SidePanel/QueryClauses/ClauseEditor.tsx (2)

23-38: Store-backed clause selection flow is consistent and avoids stale drafts

Using clause ?? clauseToAdd ?? {} gives a clear priority between an explicit clause prop and a draft clauseToAdd, and clearing clauseToAdd on submit/cancel (via setClauseToAdd(undefined)) keeps the store from retaining stale draft state. Since you access the store with .getState(), clauseToAdd is effectively read once per mount, which matches an “initialize-on-open” panel. If you ever need the editor to react to mid-session store changes, you’d want to switch to the subscribed hook form instead.


122-163: JOIN submit/cancel behavior and default props are reasonable

Clearing clauseToAdd in both handleSubmit and handleCancel ensures the panel doesn’t accidentally reuse a previous draft, and constructing the IntermediateClause from DMFormFieldValues keeps the wiring generic. For JOIN, forcing properties.type = "var" and isOuter = false is a sensible default for an inner join with type inference; if/when you introduce outer-join support, this will be an easy place to thread an isOuter toggle from the UI.

workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNode.ts (2)

62-110: Consider using forEach instead of map and strengthen the port change detection.

Line 96: Using map() without collecting the result—use forEach() for clarity since you're only iterating for side effects.

Lines 103-105: The comparison logic concatenates port IDs to detect changes. This could fail to detect order-independent changes (e.g., ["a", "b"] vs ["b", "a"]) or could have false positives if IDs overlap (e.g., ["ab", "c"] vs ["a", "bc"]).

Apply this diff:

-        if (matchedSearch && this.outPort) {
-            this.getModel().getNodes().map((node) => {
+        if (matchedSearch && this.outPort) {
+            this.getModel().getNodes().forEach((node) => {

For a more robust port change detection, consider:

                     if (prevSourcePorts.length !== this.sourcePorts.length ||
-                        prevSourcePorts.map(port => port.getID()).join('') !== this.sourcePorts.map(port => port.getID()).join('')) {
+                        !prevSourcePorts.every((port, idx) => port.getID() === this.sourcePorts[idx]?.getID())) {
                         this.shouldInitLinks = true;
                     }

Or use a Set-based approach if order doesn't matter:

const prevIds = new Set(prevSourcePorts.map(p => p.getID()));
const currentIds = new Set(this.sourcePorts.map(p => p.getID()));
const changed = prevIds.size !== currentIds.size || 
                !Array.from(prevIds).every(id => currentIds.has(id));
if (changed) {
    this.shouldInitLinks = true;
}

122-123: Potential duplicate entries in linkedPorts.

Lines 122-123 call addLinkedPort() without checking if the port already exists in the linkedPorts array. If initLinks() is called multiple times (though shouldInitLinks guards against it), or if the same port is referenced multiple times in query.inputs, this could lead to duplicate entries.

Consider adding a check in addLinkedPort or here:

             if (sourcePort) {
-                sourcePort.addLinkedPort(this.inPort);
-                sourcePort.addLinkedPort(this.targetMappedPort)
+                if (!sourcePort.linkedPorts.some(p => p.getID() === this.inPort.getID())) {
+                    sourcePort.addLinkedPort(this.inPort);
+                }
+                if (!sourcePort.linkedPorts.some(p => p.getID() === this.targetMappedPort?.getID())) {
+                    sourcePort.addLinkedPort(this.targetMappedPort);
+                }

Or, better yet, update the addLinkedPort method in InputOutputPortModel to handle deduplication internally (if not already done).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bc7ab36 and 5949afc.

📒 Files selected for processing (36)
  • workspaces/ballerina/ballerina-core/src/interfaces/data-mapper.ts (4 hunks)
  • workspaces/ballerina/ballerina-extension/src/rpc-managers/data-mapper/utils.ts (3 hunks)
  • workspaces/ballerina/ballerina-visualizer/src/views/DataMapper/DataMapperView.tsx (1 hunks)
  • workspaces/ballerina/data-mapper/src/components/DataMapper/DataMapperEditor.tsx (2 hunks)
  • workspaces/ballerina/data-mapper/src/components/DataMapper/SidePanel/QueryClauses/ClauseEditor.tsx (6 hunks)
  • workspaces/ballerina/data-mapper/src/components/DataMapper/SidePanel/QueryClauses/ClausesPanel.tsx (3 hunks)
  • workspaces/ballerina/data-mapper/src/components/DataMapper/Views/DataMapperView.ts (1 hunks)
  • workspaces/ballerina/data-mapper/src/components/Diagram/Actions/IONodesScrollCanvasAction.ts (2 hunks)
  • workspaces/ballerina/data-mapper/src/components/Diagram/Actions/utils.ts (2 hunks)
  • workspaces/ballerina/data-mapper/src/components/Diagram/Diagram.tsx (3 hunks)
  • workspaces/ballerina/data-mapper/src/components/Diagram/Label/ExpressionLabelFactory.tsx (2 hunks)
  • workspaces/ballerina/data-mapper/src/components/Diagram/Label/ExpressionLabelModel.ts (0 hunks)
  • workspaces/ballerina/data-mapper/src/components/Diagram/Label/QueryExprLabelWidget.tsx (0 hunks)
  • workspaces/ballerina/data-mapper/src/components/Diagram/Link/DataMapperLink/DataMapperLink.ts (1 hunks)
  • workspaces/ballerina/data-mapper/src/components/Diagram/LinkState/CreateLinkState.ts (4 hunks)
  • workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNode.ts (1 hunks)
  • workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNodeFactory.tsx (1 hunks)
  • workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNodeWidget.tsx (1 hunks)
  • workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/index.ts (1 hunks)
  • workspaces/ballerina/data-mapper/src/components/Diagram/Node/Input/InputNode.ts (1 hunks)
  • workspaces/ballerina/data-mapper/src/components/Diagram/Node/QueryExprConnector/QueryExprConnectorNodeWidget.tsx (1 hunks)
  • workspaces/ballerina/data-mapper/src/components/Diagram/Node/QueryOutput/QueryOutputNode.ts (0 hunks)
  • workspaces/ballerina/data-mapper/src/components/Diagram/Node/commons/DataMapperNode.ts (1 hunks)
  • workspaces/ballerina/data-mapper/src/components/Diagram/Node/index.ts (1 hunks)
  • workspaces/ballerina/data-mapper/src/components/Diagram/Port/model/InputOutputPortModel.ts (2 hunks)
  • workspaces/ballerina/data-mapper/src/components/Diagram/hooks/useDiagramModel.ts (2 hunks)
  • workspaces/ballerina/data-mapper/src/components/Diagram/utils/common-utils.ts (3 hunks)
  • workspaces/ballerina/data-mapper/src/components/Diagram/utils/focus-positioning-utils.ts (2 hunks)
  • workspaces/ballerina/data-mapper/src/components/Diagram/utils/modification-utils.ts (2 hunks)
  • workspaces/ballerina/data-mapper/src/components/Diagram/utils/node-utils.ts (1 hunks)
  • workspaces/ballerina/data-mapper/src/components/Diagram/utils/port-utils.ts (1 hunks)
  • workspaces/ballerina/data-mapper/src/store/store.ts (2 hunks)
  • workspaces/ballerina/data-mapper/src/utils/DataMapperContext/DataMapperContext.ts (2 hunks)
  • workspaces/ballerina/data-mapper/src/utils/model-utils.ts (3 hunks)
  • workspaces/ballerina/data-mapper/src/visitors/BaseVisitor.ts (2 hunks)
  • workspaces/ballerina/data-mapper/src/visitors/IntermediateNodeInitVisitor.ts (2 hunks)
💤 Files with no reviewable changes (3)
  • workspaces/ballerina/data-mapper/src/components/Diagram/Node/QueryOutput/QueryOutputNode.ts
  • workspaces/ballerina/data-mapper/src/components/Diagram/Label/ExpressionLabelModel.ts
  • workspaces/ballerina/data-mapper/src/components/Diagram/Label/QueryExprLabelWidget.tsx
🧰 Additional context used
🧬 Code graph analysis (22)
workspaces/ballerina/data-mapper/src/components/Diagram/Actions/IONodesScrollCanvasAction.ts (1)
workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNode.ts (1)
  • ClauseConnectorNode (36-187)
workspaces/ballerina/data-mapper/src/components/Diagram/utils/port-utils.ts (2)
workspaces/ballerina/data-mapper/src/components/Diagram/Node/Input/InputNode.ts (1)
  • InputNode (29-160)
workspaces/ballerina/data-mapper/src/components/Diagram/Port/model/InputOutputPortModel.ts (1)
  • InputOutputPortModel (54-123)
workspaces/ballerina/data-mapper/src/components/Diagram/Port/model/InputOutputPortModel.ts (1)
workspaces/ballerina/data-mapper/src/components/Diagram/utils/modification-utils.ts (1)
  • mapWithJoin (179-202)
workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNodeWidget.tsx (5)
workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNode.ts (1)
  • ClauseConnectorNode (36-187)
workspaces/ballerina/data-mapper/src/components/styles.ts (1)
  • useIntermediateNodeStyles (297-336)
workspaces/ballerina/data-mapper/src/store/store.ts (2)
  • useDMExpressionBarStore (135-161)
  • useDMQueryClausesPanelStore (171-180)
workspaces/ballerina/data-mapper/src/components/Diagram/Node/LinkConnector/LinkConnectorWidgetComponents.tsx (1)
  • renderPortWidget (26-32)
workspaces/common-libs/ui-toolkit/src/components/Codicon/Codicon.tsx (1)
  • Codicon (44-56)
workspaces/ballerina/data-mapper/src/components/Diagram/Actions/utils.ts (1)
workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNode.ts (1)
  • ClauseConnectorNode (36-187)
workspaces/ballerina/data-mapper/src/components/Diagram/Node/QueryExprConnector/QueryExprConnectorNodeWidget.tsx (1)
workspaces/ballerina/data-mapper/src/components/Diagram/utils/common-utils.ts (1)
  • expandArrayFn (226-266)
workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNodeFactory.tsx (2)
workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNode.ts (2)
  • ClauseConnectorNode (36-187)
  • CLAUSE_CONNECTOR_NODE_TYPE (33-33)
workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNodeWidget.tsx (1)
  • ClauseConnectorNodeWidget (35-84)
workspaces/ballerina/data-mapper/src/components/DataMapper/DataMapperEditor.tsx (1)
workspaces/ballerina/data-mapper/src/components/DataMapper/Views/DataMapperView.ts (1)
  • View (18-23)
workspaces/ballerina/data-mapper/src/components/DataMapper/SidePanel/QueryClauses/ClauseEditor.tsx (2)
workspaces/ballerina/data-mapper/src/store/store.ts (1)
  • useDMQueryClausesPanelStore (171-180)
workspaces/ballerina/ballerina-core/src/interfaces/data-mapper.ts (4)
  • DMFormField (260-271)
  • DMFormFieldValues (273-275)
  • IntermediateClause (216-219)
  • IntermediateClauseProps (206-214)
workspaces/ballerina/data-mapper/src/visitors/BaseVisitor.ts (1)
workspaces/ballerina/ballerina-core/src/interfaces/data-mapper.ts (1)
  • ExpandedDMModel (118-129)
workspaces/ballerina/data-mapper/src/components/DataMapper/SidePanel/QueryClauses/ClausesPanel.tsx (1)
workspaces/ballerina/data-mapper/src/store/store.ts (1)
  • useDMQueryClausesPanelStore (171-180)
workspaces/ballerina/data-mapper/src/components/Diagram/utils/node-utils.ts (2)
workspaces/ballerina/data-mapper/src/components/DataMapper/Views/DataMapperView.ts (1)
  • View (18-23)
workspaces/ballerina/data-mapper/src/components/Diagram/Node/Input/InputNode.ts (1)
  • InputNode (29-160)
workspaces/ballerina/data-mapper/src/store/store.ts (1)
workspaces/ballerina/ballerina-core/src/interfaces/data-mapper.ts (1)
  • IntermediateClause (216-219)
workspaces/ballerina/data-mapper/src/components/Diagram/LinkState/CreateLinkState.ts (2)
workspaces/ballerina/data-mapper/src/components/Diagram/Port/model/InputOutputPortModel.ts (1)
  • InputOutputPortModel (54-123)
workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNode.ts (1)
  • ClauseConnectorNode (36-187)
workspaces/ballerina/data-mapper/src/components/Diagram/utils/focus-positioning-utils.ts (1)
workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNode.ts (1)
  • ClauseConnectorNode (36-187)
workspaces/ballerina/data-mapper/src/visitors/IntermediateNodeInitVisitor.ts (2)
workspaces/ballerina/ballerina-core/src/interfaces/data-mapper.ts (1)
  • Query (191-198)
workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNode.ts (1)
  • ClauseConnectorNode (36-187)
workspaces/ballerina/data-mapper/src/utils/model-utils.ts (2)
workspaces/ballerina/ballerina-core/src/interfaces/data-mapper.ts (1)
  • ExpandedDMModel (118-129)
workspaces/ballerina/data-mapper/src/visitors/BaseVisitor.ts (1)
  • BaseVisitor (21-40)
workspaces/ballerina/ballerina-extension/src/rpc-managers/data-mapper/utils.ts (1)
workspaces/ballerina/ballerina-core/src/interfaces/data-mapper.ts (1)
  • DMModel (131-144)
workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNode.ts (8)
workspaces/ballerina/data-mapper/src/components/Diagram/Port/model/InputOutputPortModel.ts (1)
  • InputOutputPortModel (54-123)
workspaces/ballerina/ballerina-core/src/interfaces/data-mapper.ts (1)
  • DMDiagnostic (70-83)
workspaces/ballerina/data-mapper/src/utils/DataMapperContext/DataMapperContext.ts (1)
  • IDataMapperContext (21-35)
workspaces/ballerina/data-mapper/src/store/store.ts (1)
  • useDMSearchStore (83-89)
workspaces/ballerina/data-mapper/src/components/Diagram/utils/node-utils.ts (1)
  • findInputNode (23-47)
workspaces/ballerina/data-mapper/src/components/Diagram/utils/port-utils.ts (2)
  • getInputPort (26-37)
  • getTargetPortPrefix (60-73)
workspaces/ballerina/data-mapper/src/components/Diagram/Node/QueryOutput/QueryOutputNode.ts (1)
  • QueryOutputNode (37-186)
workspaces/ballerina/data-mapper/src/components/Diagram/Link/DataMapperLink/DataMapperLink.ts (1)
  • DataMapperLinkModel (37-95)
workspaces/ballerina/data-mapper/src/components/Diagram/Diagram.tsx (1)
workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNode.ts (1)
  • ClauseConnectorNode (36-187)
workspaces/ballerina/data-mapper/src/components/Diagram/utils/common-utils.ts (1)
workspaces/ballerina/data-mapper/src/utils/DataMapperContext/DataMapperContext.ts (1)
  • IDataMapperContext (21-35)
workspaces/ballerina/data-mapper/src/components/Diagram/utils/modification-utils.ts (3)
workspaces/ballerina/data-mapper/src/components/Diagram/utils/common-utils.ts (1)
  • expandArrayFn (226-266)
workspaces/ballerina/data-mapper/src/components/Diagram/Port/model/InputOutputPortModel.ts (1)
  • InputOutputPortModel (54-123)
workspaces/ballerina/data-mapper/src/store/store.ts (1)
  • useDMQueryClausesPanelStore (171-180)
🔇 Additional comments (30)
workspaces/ballerina/ballerina-visualizer/src/views/DataMapper/DataMapperView.tsx (1)

716-721: Including query inputs in the signature is correct; confirm query.inputs are names (strings)

The new inputs construction correctly ensures that changes to model.query.inputs will now be reflected in the model signature and thus in hasInputsChanged, which is important for reprocessing when query inputs change.

This relies on model.query?.inputs being an array of input names (strings) to stay consistent with ModelSignature.inputs: string[] and with the existing model.inputs.map(i => i.name).

If query.inputs instead contains IO/field objects, consider normalizing to names as well:

const getModelSignature = (model: DMModel | ExpandedDMModel): ModelSignature => ({
    inputs: [
        ...model.inputs.map(i => i.name),
        ...(model.query?.inputs
            ? model.query.inputs.map(i => typeof i === "string" ? i : i.name)
            : []),
    ],
    output: model.output.name,
    subMappings: model.subMappings?.map(s => (s as IORoot | IOType).name) || [],
    refs: "refs" in model ? JSON.stringify(model.refs) : "",
});

If query.inputs is already typed as string[] in DMModel, the current implementation is fine and this comment can be ignored.

workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/index.ts (1)

18-20: LGTM!

The barrel exports follow the standard pattern and properly consolidate the ClauseConnector module exports.

workspaces/ballerina/data-mapper/src/components/Diagram/Node/index.ts (1)

28-28: LGTM!

The ClauseConnector export is properly added to the Node module's public API, consistent with other node type exports.

workspaces/ballerina/data-mapper/src/components/Diagram/Actions/utils.ts (2)

28-29: LGTM!

The imports are properly added to support the new ClauseConnectorNode functionality.


46-50: LGTM!

ClauseConnectorNode is correctly added to the INTERMEDIATE_NODES array, consistent with other connector node types.

workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNodeFactory.tsx (1)

31-38: LGTM!

The conditional rendering logic correctly ensures the widget is only displayed when both input and output ports have established links.

workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNodeWidget.tsx (2)

35-52: LGTM!

The component properly integrates with the data mapper stores and provides appropriate handlers for opening the clause panel and editing diagnostics.


60-83: LGTM!

The widget layout properly renders ports, action buttons, and diagnostic information with appropriate styling and accessibility attributes.

workspaces/ballerina/data-mapper/src/components/Diagram/Actions/IONodesScrollCanvasAction.ts (2)

30-30: LGTM!

The ClauseConnectorNode import is properly added to support the new node type in scroll handling.


164-164: LGTM!

ClauseConnectorNode is correctly included in the instanceof check for intermediate node repositioning, ensuring consistent behavior with other connector node types.

workspaces/ballerina/data-mapper/src/components/Diagram/Node/commons/DataMapperNode.ts (1)

383-391: The referenced resolveArrayMemberField helper does not exist in the codebase.

The review comment's concern appears to be based on a non-existent refactoring. The code is correct:

  • attributes.field?.member directly accesses the array member type (standard IOType property)
  • focusedFieldFQNs is preserved via the ...attributes spread operator passed to addPortsForInputField
  • Focus resolution continues through the isFocusedField() method calls during recursive field processing (lines 119, 238)

Array field focus behavior is maintained through the existing focused field tracking mechanism; no prior helper function needs verification.

workspaces/ballerina/data-mapper/src/components/DataMapper/Views/DataMapperView.ts (1)

20-20: All consumers properly handle the breaking API change.

The sourceFieldsourceFields (string array) migration has been correctly implemented:

  • View interface (DataMapperView.ts line 20): Updated to sourceFields?: string[]
  • View instantiation (common-utils.ts line 250): Correctly sets sourceFields: inputIds
  • View consumption (InputNode.ts line 58): Properly accesses with flatMap(view => view.sourceFields).filter(Boolean) to handle the optional array
  • Initial view (DataMapperEditor.tsx line 153): Omits sourceFields safely (optional property)

No references to the old sourceField property remain in the codebase.

workspaces/ballerina/data-mapper/src/components/Diagram/Node/QueryExprConnector/QueryExprConnectorNodeWidget.tsx (1)

62-77: Expand array focus to use all mapping inputs

Passing mapping.inputs into expandArrayFn matches the updated inputIds: string[] signature and lets the expansion logic consider all query inputs instead of just the first one. This is consistent with multi‑input mappings (e.g., JOIN) and looks correct assuming mapping.inputs is always a defined string array.

workspaces/ballerina/data-mapper/src/components/Diagram/utils/focus-positioning-utils.ts (1)

18-168: Extend focus resolution to ClauseConnectorNode

Including ClauseConnectorNode in the intermediate-node union inside resolveTargetNodeAndPort correctly re-routes focus to the underlying targetMappedPort for JOIN clause connectors, keeping behavior consistent with other intermediate nodes. The undefined‑port case falls back to null cleanly, so this looks safe.

workspaces/ballerina/data-mapper/src/components/Diagram/Link/DataMapperLink/DataMapperLink.ts (1)

27-35: Add ArrayJoin as a dedicated mapping type

Introducing MappingType.ArrayJoin = "array-join" cleanly separates JOIN behavior from other array mapping types and provides a clear discriminator for downstream UI/logic. No issues here.

workspaces/ballerina/data-mapper/src/components/DataMapper/DataMapperEditor.tsx (1)

153-158: Annotate initialView as View[]

The explicit View[] annotation for initialView lines up with the View interface and keeps the reducer state well-typed. No issues here.

workspaces/ballerina/data-mapper/src/utils/model-utils.ts (1)

19-88: Integrate query nodes into the traversal pipeline

Hooking model.query into traverseNode via traverseQuery, and delegating to beginVisitQuery/endVisitQuery, cleanly extends the existing visitor pattern without disturbing inputs/outputs/mappings traversal. This gives visitors (e.g., IntermediateNodeInitVisitor) a well-defined entry point for query/JOIN logic. Looks good.

workspaces/ballerina/data-mapper/src/components/Diagram/Port/model/InputOutputPortModel.ts (1)

21-89: Trigger JOIN-clause flow for ArrayJoin mappings

Extending the isPendingMappingRequired branch to special-case MappingType.ArrayJoin and invoke mapWithJoin(lm) before returning cleanly diverts JOIN connections into the query-clauses flow instead of creating a regular mapping. Given mapWithJoin already null‑checks the source port, this wiring looks correct and keeps the existing behavior for other mapping types intact.

workspaces/ballerina/data-mapper/src/components/Diagram/utils/port-utils.ts (1)

26-37: getInputPort SubMappingNode support looks correct

The widened signature and conditional portId construction are consistent with existing patterns and keep the hidden‑parent walking behavior intact. No issues from a typing or runtime perspective.

workspaces/ballerina/data-mapper/src/components/DataMapper/SidePanel/QueryClauses/ClausesPanel.tsx (2)

19-21: React import and hook usage are fine

Using import React, { useEffect } from "react"; together with React.useState and useEffect is consistent and safe; no concerns here.


81-88: Panel width increase

Changing the SidePanel width to 400 should just be a layout tweak. Verify visually that it doesn’t collide with other UI elements in typical editor window sizes.

workspaces/ballerina/data-mapper/src/visitors/BaseVisitor.ts (1)

19-39: Query visitor hooks are a clean extension

Adding beginVisitQuery/endVisitQuery with Query from the core library is a straightforward and backwards‑compatible extension of BaseVisitor. This matches how ExpandedDMModel already carries query?: Query.

workspaces/ballerina/data-mapper/src/utils/DataMapperContext/DataMapperContext.ts (1)

21-25: Adding hasInputsOutputsChanged to context is reasonable

The new hasInputsOutputsChanged flag on IDataMapperContext and DataMapperContext cleanly exposes IO change tracking to consumers. With the default value in the constructor, existing call sites compile as long as their argument ordering has been adjusted where the flag is used (as done in DataMapperEditor).

Also applies to: 37-53

workspaces/ballerina/data-mapper/src/components/Diagram/utils/modification-utils.ts (1)

25-29: JOIN mapping wiring via mapWithJoin looks correct; be mindful of field assumptions

  • Updating mapWithQuery to call expandArrayFn(context, [input], ...) correctly matches the new inputIds: string[] API without changing behavior.
  • mapWithJoin cleanly:
    • Extracts the source port,
    • Seeds a JOIN-type IntermediateClause with a generated name (<fieldName>Item), type: "var", and the source fieldFQN as expression,
    • Opens the query clauses panel via the store.

Two minor points to keep in mind:

  1. sourcePortModel.attributes.field and .fieldFQN are assumed to be defined. This matches existing usage in getMapWithFnData, but if you ever allow joins from ports that don’t map to a concrete field (e.g., header-only ports), you may want to guard:

    if (!sourcePortModel.attributes.field || !sourcePortModel.attributes.fieldFQN) {
        return;
    }
  2. Please verify that the initial JOIN clause shape (type: "var", isOuter: false, empty lhsExpression/rhsExpression) matches what ClauseEditor expects to receive for a queued JOIN.

Overall, the mapping and store interactions look sound.

Also applies to: 153-177, 179-202

workspaces/ballerina/data-mapper/src/components/DataMapper/SidePanel/QueryClauses/ClauseEditor.tsx (1)

40-83: JOIN-specific form configuration looks coherent with the new clause props

The extended clauseTypeItems, conditional labels/documentation for JOIN, and the additional lhsExpression/rhsExpression fields all line up with IntermediateClauseProps and with how JOINs are typically modeled (join source, item alias, and ON condition sides). The generateFields switch producing [expressionField, nameField, lhsExpressionField, rhsExpressionField] for JOIN keeps the form structure clear and avoids surfacing irrelevant controls like orderField.

Also applies to: 98-120, 140-153

workspaces/ballerina/ballerina-core/src/interfaces/data-mapper.ts (1)

56-63: JOIN clause and focus-mapping extensions align with consumer code

The additions here are cohesive:

  • IntermediateClauseType.JOIN matches the new JOIN option in ClauseEditor.
  • Optional lhsExpression, rhsExpression, and isOuter on IntermediateClauseProps cover the extra metadata JOIN needs without impacting existing clauses.
  • focusInputRootMap? on both ExpandedDMModel and DMModel, plus traversingRoot? on DMModel, align with the updated findInputNode and other JOIN-aware traversal logic.

Because all the new fields are optional, these changes are backward-compatible for consumers that aren’t JOIN-aware yet.

Also applies to: 118-129, 131-144, 206-214

workspaces/ballerina/data-mapper/src/components/Diagram/utils/common-utils.ts (2)

102-113: Array join mapping detection via port suffix is reasonable

The new branch in getMappingType:

if (sourceDim > 0) {
    const dimDelta = sourceDim - targetDim;
    if (dimDelta == 0) {
        if (targetPort.attributes.portName.endsWith(".#")) {
            return MappingType.ArrayJoin;
        }
        return MappingType.ArrayToArray;
    }
    if (dimDelta > 0) {
        return MappingType.ArrayToSingleton;
    }
}

cleanly distinguishes ArrayJoin from a regular ArrayToArray map using the ".#" suffix convention on the target port name, while preserving previous behavior for other cases. As long as join targets consistently use that suffix, this keeps the mapping type classification straightforward.


226-266: All callers of expandArrayFn are correctly updated to pass inputIds: string[]

Verification confirms both call sites in the Ballerina workspace properly pass string arrays:

  1. QueryExprConnectorNodeWidget.tsx:76 — passes mapping.inputs which is confirmed as string[] (evidenced by .join() call in QueryExprConnectorNode.ts line 68)
  2. modification-utils.ts:176 — passes [input] wrapping the single input in an array for the single-input case

The breaking change has been successfully applied across all relevant call sites.

workspaces/ballerina/data-mapper/src/components/Diagram/LinkState/CreateLinkState.ts (1)

33-33: LGTM! ClauseConnectorNode integration into link creation flow.

The changes correctly integrate ClauseConnectorNode into the link creation state machine by:

  • Importing the new node type alongside LinkConnectorNode
  • Removing isQueryHeaderPort restrictions that previously prevented query header ports from participating in link creation
  • Adding ClauseConnectorNode to the parent node resolution logic so that intermediate ports on clause connectors properly resolve to their targetMappedPort

This enables clause-based joins to work seamlessly within the existing link creation workflow.

Also applies to: 100-109, 123-143, 145-191

workspaces/ballerina/data-mapper/src/components/Diagram/Diagram.tsx (1)

39-39: LGTM! Proper registration and integration of ClauseConnectorNode.

The changes follow the established pattern for integrating connector nodes:

  • Importing ClauseConnectorNode alongside existing connector node types
  • Registering ClauseConnectorNodeFactory with the diagram engine
  • Including ClauseConnectorNode in the node filter and type union for position updates

This ensures clause connectors are properly positioned based on their target ports, consistent with other connector node types.

Also applies to: 90-90, 161-169

@madushajg madushajg requested a review from Copilot November 20, 2025 06:02
Copilot finished reviewing on behalf of madushajg November 20, 2025 06:05
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR adds JOIN clause support to the BI Data Mapper, enabling users to perform join operations on collections. The implementation includes a new clause connector visualization component, extends the data model to support multiple source fields in mappings, and updates the query clause editor with JOIN-specific form fields.

Key Changes

  • Added IntermediateClauseType.JOIN enum value and related properties (lhsExpression, rhsExpression, isOuter) to support join condition configuration
  • Introduced ClauseConnectorNode component for visualizing clause operations in the diagram, similar to existing connector nodes
  • Refactored view model from single sourceField to array sourceFields to support multiple input sources for join operations

Reviewed Changes

Copilot reviewed 36 out of 36 changed files in this pull request and generated 22 comments.

Show a summary per file
File Description
ballerina-core/src/interfaces/data-mapper.ts Added JOIN clause type enum and extended interface properties for join conditions
data-mapper/src/visitors/BaseVisitor.ts Added query visiting methods to support traversing query structures
data-mapper/src/visitors/IntermediateNodeInitVisitor.ts Implemented clause connector node creation during query visits
data-mapper/src/utils/model-utils.ts Added query traversal logic to model visiting
data-mapper/src/components/Diagram/Node/ClauseConnector/* New clause connector node component with widget, factory, and model
data-mapper/src/components/Diagram/utils/modification-utils.ts Added mapWithJoin function to handle JOIN clause mapping initialization
data-mapper/src/components/Diagram/utils/common-utils.ts Added ArrayJoin mapping type detection and updated expandArrayFn signature
data-mapper/src/components/Diagram/Link/DataMapperLink/DataMapperLink.ts Added ArrayJoin mapping type enum value
data-mapper/src/components/DataMapper/SidePanel/QueryClauses/ClauseEditor.tsx Extended form fields for JOIN clause configuration (LHS/RHS expressions)
data-mapper/src/components/DataMapper/Views/DataMapperView.ts Changed view model from single sourceField to array sourceFields
data-mapper/src/store/store.ts Added clauseToAdd state for pre-populating clause editor
ballerina-extension/src/rpc-managers/data-mapper/utils.ts Added focusInputRootMap tracking for mapping focused inputs to their root sources

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

let inputNode = findNodeByField(fieldStartsWith);

// if not found, try with parentSourceField
if (!inputNode && views && lastViewIndex) {
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

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

Potential issue: focusInputRootMap might be undefined if not initialized. Add a safety check before accessing it:

if (!inputNode && views && lastViewIndex && outputNode.context.model.focusInputRootMap) {
    inputNode = findNodeByField(outputNode.context.model.focusInputRootMap[fieldStartsWith]);
}
Suggested change
if (!inputNode && views && lastViewIndex) {
if (
!inputNode &&
views &&
lastViewIndex &&
outputNode.context.model.focusInputRootMap
) {

Copilot uses AI. Check for mistakes.
Comment on lines 103 to 104
if (prevSourcePorts.length !== this.sourcePorts.length ||
prevSourcePorts.map(port => port.getID()).join('') !== this.sourcePorts.map(port => port.getID()).join('')) {
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

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

The condition for setting shouldInitLinks compares port ID strings by joining them, which could produce false positives if individual IDs contain the same characters in different arrangements. Consider comparing arrays properly:

if (prevSourcePorts.length !== this.sourcePorts.length ||
    !prevSourcePorts.every((port, idx) => port.getID() === this.sourcePorts[idx]?.getID())) {
    this.shouldInitLinks = true;
}
Suggested change
if (prevSourcePorts.length !== this.sourcePorts.length ||
prevSourcePorts.map(port => port.getID()).join('') !== this.sourcePorts.map(port => port.getID()).join('')) {
if (
prevSourcePorts.length !== this.sourcePorts.length ||
!prevSourcePorts.every((port, idx) => port.getID() === this.sourcePorts[idx]?.getID())
) {

Copilot uses AI. Check for mistakes.
optional: false,
editable: true,
documentation: "Enter the name of the tool.",
documentation: clauseType === IntermediateClauseType.JOIN ? "Represents each record in the joined collection" : "Enter the name for variable",
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

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

Missing article "the" before "variable". Should be "Enter the name for the variable".

Suggested change
documentation: clauseType === IntermediateClauseType.JOIN ? "Represents each record in the joined collection" : "Enter the name for variable",
documentation: clauseType === IntermediateClauseType.JOIN ? "Represents each record in the joined collection" : "Enter the name for the variable",

Copilot uses AI. Check for mistakes.
import { InputOutputPortModel, IntermediatePortModel } from "../../Port";
import { DataMapperNodeModel } from "../commons/DataMapperNode";
import { ArrayOutputNode } from "../ArrayOutput";
import { ObjectOutputNode } from "../ObjectOutput";
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

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

Unused import ObjectOutputNode.

Suggested change
import { ObjectOutputNode } from "../ObjectOutput";

Copilot uses AI. Check for mistakes.
import { ArrayOutputNode } from "../ArrayOutput";
import { ObjectOutputNode } from "../ObjectOutput";
import { findInputNode } from "../../utils/node-utils";
import { getInputPort, getOutputPort, getTargetPortPrefix } from "../../utils/port-utils";
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

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

Unused import getOutputPort.

Suggested change
import { getInputPort, getOutputPort, getTargetPortPrefix } from "../../utils/port-utils";
import { getInputPort, getTargetPortPrefix } from "../../utils/port-utils";

Copilot uses AI. Check for mistakes.
import { findInputNode } from "../../utils/node-utils";
import { getInputPort, getOutputPort, getTargetPortPrefix } from "../../utils/port-utils";
import { OFFSETS } from "../../utils/constants";
import { removeMapping } from "../../utils/modification-utils";
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

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

Unused import removeMapping.

Suggested change
import { removeMapping } from "../../utils/modification-utils";

Copilot uses AI. Check for mistakes.
Comment on lines 54 to 59
const loadingScreen = (
<div className={classnames(classes.element, classes.loadingContainer)}>
<ProgressRing sx={{ height: '16px', width: '16px' }} />
</div>
);

Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

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

Unused variable loadingScreen.

Suggested change
const loadingScreen = (
<div className={classnames(classes.element, classes.loadingContainer)}>
<ProgressRing sx={{ height: '16px', width: '16px' }} />
</div>
);

Copilot uses AI. Check for mistakes.
this.sourcePorts.push(inputPort);
}
}
})
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

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

Avoid automated semicolon insertion (93% of all statements in the enclosing function have an explicit semicolon).

Suggested change
})
});

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@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: 3

♻️ Duplicate comments (3)
workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNode.ts (1)

83-86: Optimize duplicate port check for better performance.

The duplicate check !this.sourcePorts.some(port => port.getID() === inputPort.getID()) runs inside the forEach loop for each input. With many inputs, this O(n²) check could become inefficient.

Consider using a Set to track port IDs for O(1) lookups:

+        const existingPortIds = new Set<string>();
         this.query.inputs.forEach((field) => {
             const inputField = field?.split('.').pop();
             const matchedSearch = inputSearch === "" || inputField.toLowerCase().includes(inputSearch.toLowerCase());
 
             if (!matchedSearch) return;
 
             const inputNode = findInputNode(field, this, views, lastViewIndex);
             if (inputNode) {
                 const inputPort = getInputPort(inputNode, field?.replace(/\.\d+/g, ''));
-                if (!this.sourcePorts.some(port => port.getID() === inputPort.getID())) {
+                const portId = inputPort.getID();
+                if (!existingPortIds.has(portId)) {
                     this.sourcePorts.push(inputPort);
+                    existingPortIds.add(portId);
                 }
             }
         });

Based on past review comments.

workspaces/ballerina/data-mapper/src/components/DataMapper/SidePanel/QueryClauses/ClauseEditor.tsx (2)

23-23: Prefer subscribing to the store via the hook instead of getState()

Reading clauseToAdd from useDMQueryClausesPanelStore.getState() means ClauseEditor will not re-render if clauseToAdd changes while the component stays mounted, and it diverges from the usual Zustand hook pattern. Consider subscribing via the hook so the active clause stays in sync:

-    const { clauseToAdd, setClauseToAdd } = useDMQueryClausesPanelStore.getState();
-    const { type: _clauseType, properties: clauseProps } = clause ?? clauseToAdd ?? {};
+    const { clauseToAdd } = useDMQueryClausesPanelStore();
+    const setClauseToAdd = useDMQueryClausesPanelStore((state) => state.setClauseToAdd);
+    const { type: _clauseType, properties: clauseProps } = clause ?? clauseToAdd ?? {};

This keeps the editor reactive to store updates and consistent with other components using the same store.

Also applies to: 36-38


98-120: JOIN fields look correct; consider isolating JOIN-specific defaults from the generic submit handler

The new lhsExpression/rhsExpression fields and the JOIN branch in generateFields() align with IntermediateClauseProps and should work as expected for editing JOIN clauses.

The JOIN-specific defaults in handleSubmit:

if (clauseType === IntermediateClauseType.JOIN) {
    clause.properties.type = "var";
    clause.properties.isOuter = false;
}

hard‑code JOIN semantics into this otherwise generic editor. As JOIN evolves (e.g., supporting outer joins or different binding types), this coupling can make it harder to extend without revisiting this component.

Consider moving these defaults into either:

  • A small JOIN-specific helper (e.g., normalizeJoinClauseProps(data)), or
  • The place where IntermediateClauseProps for JOIN are defined/generated,

so ClauseEditor just submits data and stays agnostic to clause‑specific rules.

Also applies to: 128-131, 148-149

🧹 Nitpick comments (1)
workspaces/ballerina/data-mapper/src/components/DataMapper/SidePanel/QueryClauses/ClauseEditor.tsx (1)

122-137: Ensure setClauseToAdd’s type allows clearing with undefined

Both handleSubmit and handleCancel call setClauseToAdd(undefined) to clear the draft, which is a good pattern. However, in store.ts the setter is currently typed as taking IntermediateClause only (per the provided snippet), which would be a TypeScript error if not updated.

If you haven’t already, please widen the setter signature so the call here is type-safe:

-    setClauseToAdd: (clauseToAdd: IntermediateClause) => set({ clauseToAdd }),
+    setClauseToAdd: (clauseToAdd: IntermediateClause | undefined) => set({ clauseToAdd }),

Alternatively, you could expose and use a dedicated resetQueryClausesPanelStore helper here, but allowing undefined is simplest given the state shape.

Also applies to: 160-163

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 75d4c7b and f6a978a.

📒 Files selected for processing (4)
  • workspaces/ballerina/data-mapper/src/components/DataMapper/SidePanel/QueryClauses/ClauseEditor.tsx (6 hunks)
  • workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNode.ts (1 hunks)
  • workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNodeWidget.tsx (1 hunks)
  • workspaces/ballerina/data-mapper/src/components/Diagram/utils/node-utils.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • workspaces/ballerina/data-mapper/src/components/Diagram/utils/node-utils.ts
🧰 Additional context used
🧬 Code graph analysis (3)
workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNodeWidget.tsx (4)
workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNode.ts (1)
  • ClauseConnectorNode (33-184)
workspaces/ballerina/data-mapper/src/store/store.ts (2)
  • useDMExpressionBarStore (135-161)
  • useDMQueryClausesPanelStore (171-180)
workspaces/ballerina/data-mapper/src/components/Diagram/Node/LinkConnector/LinkConnectorWidgetComponents.tsx (1)
  • renderPortWidget (26-32)
workspaces/common-libs/ui-toolkit/src/components/Codicon/Codicon.tsx (1)
  • Codicon (44-56)
workspaces/ballerina/data-mapper/src/components/DataMapper/SidePanel/QueryClauses/ClauseEditor.tsx (2)
workspaces/ballerina/data-mapper/src/store/store.ts (1)
  • useDMQueryClausesPanelStore (171-180)
workspaces/ballerina/ballerina-core/src/interfaces/data-mapper.ts (4)
  • DMFormField (260-271)
  • DMFormFieldValues (273-275)
  • IntermediateClause (216-219)
  • IntermediateClauseProps (206-214)
workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNode.ts (8)
workspaces/ballerina/data-mapper/src/components/Diagram/Port/model/InputOutputPortModel.ts (1)
  • InputOutputPortModel (54-123)
workspaces/ballerina/ballerina-core/src/interfaces/data-mapper.ts (1)
  • DMDiagnostic (70-83)
workspaces/ballerina/data-mapper/src/utils/DataMapperContext/DataMapperContext.ts (1)
  • IDataMapperContext (21-35)
workspaces/ballerina/data-mapper/src/store/store.ts (1)
  • useDMSearchStore (83-89)
workspaces/ballerina/data-mapper/src/components/Diagram/utils/node-utils.ts (1)
  • findInputNode (23-47)
workspaces/ballerina/data-mapper/src/components/Diagram/utils/port-utils.ts (2)
  • getInputPort (26-37)
  • getTargetPortPrefix (60-73)
workspaces/ballerina/data-mapper/src/components/Diagram/Node/QueryOutput/QueryOutputNode.ts (1)
  • QueryOutputNode (37-186)
workspaces/ballerina/data-mapper/src/components/Diagram/Link/DataMapperLink/DataMapperLink.ts (1)
  • DataMapperLinkModel (37-95)
🔇 Additional comments (4)
workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNode.ts (2)

169-179: LGTM!

The position update logic correctly guards against missing targetMappedPort and appropriately adjusts positioning based on error state.


181-183: LGTM!

The error check properly uses optional chaining to handle cases where diagnostics might be undefined.

workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNodeWidget.tsx (1)

54-77: LGTM!

The component rendering logic is clean and well-structured. The use of optional chaining in the data-testid attribute (line 62) demonstrates good defensive coding practices. The conditional rendering of the diagnostic widget is appropriate.

workspaces/ballerina/data-mapper/src/components/DataMapper/SidePanel/QueryClauses/ClauseEditor.tsx (1)

40-47: JOIN type option and conditional labels/documentation look consistent

The additions for IntermediateClauseType.FROM/JOIN in clauseTypeItems and the JOIN‑specific labels/documentation for name and expression fields (“Item Alias”, “Represents each record in the joined collection”, “Join With Collection”, “Collection to be joined”) read clearly and match the intended JOIN semantics. No changes needed here.

Also applies to: 49-56, 73-80

Comment on lines +114 to +137
this.sourcePorts.forEach((sourcePort) => {
const inPort = this.inPort;
const lm = new DataMapperLinkModel(undefined, this.diagnostics, true, undefined, true);

if (sourcePort) {
sourcePort.addLinkedPort(this.inPort);
sourcePort.addLinkedPort(this.targetMappedPort)

lm.setTargetPort(this.inPort);
lm.setSourcePort(sourcePort);
lm.registerListener({
selectionChanged(event) {
if (event.isSelected) {
inPort.fireEvent({}, "link-selected");
sourcePort.fireEvent({}, "link-selected");
} else {
inPort.fireEvent({}, "link-unselected");
sourcePort.fireEvent({}, "link-unselected");
}
},
})
this.getModel().addAll(lm);
}
})
Copy link
Contributor

@coderabbitai coderabbitai bot Nov 20, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard against undefined targetMappedPort when adding linked ports.

Line 120 calls sourcePort.addLinkedPort(this.targetMappedPort) without verifying that targetMappedPort is defined. Since targetMappedPort is only set when the output search matches and a QueryOutputNode is found (lines 92-106), it could be undefined here, adding an invalid entry to the linkedPorts array.

Apply this diff to add a guard:

         this.sourcePorts.forEach((sourcePort) => {
             const inPort = this.inPort;
             const lm = new DataMapperLinkModel(undefined, this.diagnostics, true, undefined, true);
 
             if (sourcePort) {
                 sourcePort.addLinkedPort(this.inPort);
-                sourcePort.addLinkedPort(this.targetMappedPort)
+                if (this.targetMappedPort) {
+                    sourcePort.addLinkedPort(this.targetMappedPort);
+                }
 
                 lm.setTargetPort(this.inPort);
                 lm.setSourcePort(sourcePort);
📝 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.

Suggested change
this.sourcePorts.forEach((sourcePort) => {
const inPort = this.inPort;
const lm = new DataMapperLinkModel(undefined, this.diagnostics, true, undefined, true);
if (sourcePort) {
sourcePort.addLinkedPort(this.inPort);
sourcePort.addLinkedPort(this.targetMappedPort)
lm.setTargetPort(this.inPort);
lm.setSourcePort(sourcePort);
lm.registerListener({
selectionChanged(event) {
if (event.isSelected) {
inPort.fireEvent({}, "link-selected");
sourcePort.fireEvent({}, "link-selected");
} else {
inPort.fireEvent({}, "link-unselected");
sourcePort.fireEvent({}, "link-unselected");
}
},
})
this.getModel().addAll(lm);
}
})
this.sourcePorts.forEach((sourcePort) => {
const inPort = this.inPort;
const lm = new DataMapperLinkModel(undefined, this.diagnostics, true, undefined, true);
if (sourcePort) {
sourcePort.addLinkedPort(this.inPort);
if (this.targetMappedPort) {
sourcePort.addLinkedPort(this.targetMappedPort);
}
lm.setTargetPort(this.inPort);
lm.setSourcePort(sourcePort);
lm.registerListener({
selectionChanged(event) {
if (event.isSelected) {
inPort.fireEvent({}, "link-selected");
sourcePort.fireEvent({}, "link-selected");
} else {
inPort.fireEvent({}, "link-unselected");
sourcePort.fireEvent({}, "link-unselected");
}
},
})
this.getModel().addAll(lm);
}
})
🤖 Prompt for AI Agents
In
workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNode.ts
around lines 114 to 137, sourcePort.addLinkedPort(this.targetMappedPort) is
called without checking if targetMappedPort is defined which can push undefined
into linkedPorts; before calling addLinkedPort for targetMappedPort, guard it
with an if (this.targetMappedPort) (or explicit null/undefined check) so you
only add a linked port when targetMappedPort is set, keeping the existing
behavior for inPort and preserving link creation and listener registration.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If targetPort did not find code should hit ErrorBoundary

Copy link
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the addLinkedPort method implementation to check if it validates the input
ast-grep --pattern $'addLinkedPort($$$) {
  $$$
}'

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Also search for the addLinkedPort definition in port-related files
rg -n "addLinkedPort" --type=ts -A 5 -B 2

Length of output: 21921


🏁 Script executed:

#!/bin/bash
# Check how linkedPorts is used - does it access properties that would throw on undefined?
rg -n "linkedPorts" --type=ts -A 3 -B 1 | head -100

Length of output: 10344


🏁 Script executed:

#!/bin/bash
# Check the ClauseConnectorNodeFactory to see if there's a guard preventing rendering when targetMappedPort is undefined
cat workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNodeFactory.tsx

Length of output: 1620


🏁 Script executed:

#!/bin/bash
# Check if initLinks is only called when targetMappedPort is set
rg -n "initLinks" --type=ts -B 3 -A 3 workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/

Length of output: 835


@KCSAbeywickrama, I understand your point about ErrorBoundary handling error conditions. However, there are a few concerns with the current approach:

  1. Silent failure: At line 98, getPort() can return undefined if the port doesn't exist, and shouldInitLinks at line 101 can still be set to true based on sourcePorts changes. This means targetMappedPort could be undefined when entering initLinks().

  2. Delayed error: If undefined is added to linkedPorts, the error won't occur immediately. It will only throw when linkedPorts.forEach((linkedPort) => linkedPort.fireEvent(...)) is called later (e.g., in CreateLinkState.ts line 128), which might happen during event handling outside the ErrorBoundary's scope.

  3. Unclear intent: Without an explicit guard or error throw, it's unclear whether an undefined targetMappedPort is an expected edge case or a critical error.

Suggestion: If not finding the target port is truly an error condition that should be caught, consider either:

  • Adding an explicit guard with a meaningful error:
    if (!this.targetMappedPort) {
        throw new Error(`Target port not found for query output: ${this.query.output}`);
    }
  • Or adding the null check before addLinkedPort to prevent undefined from entering the array in the first place.

This makes the error handling explicit and ensures the ErrorBoundary can catch it at the right time. What's your preference?

Comment on lines +49 to +52
const onClickEdit = () => {
const targetPort = node.targetMappedPort;
setExprBarFocusedPort(targetPort);
};
Copy link
Contributor

@coderabbitai coderabbitai bot Nov 20, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add guard for undefined targetMappedPort in edit handler.

The onClickEdit handler accesses node.targetMappedPort without checking if it's defined. Since targetMappedPort is only set when the output search matches and a QueryOutputNode is found in initPorts(), it could be undefined, causing setExprBarFocusedPort to receive an invalid value.

Apply this diff to add a guard:

     const onClickEdit = () => {
+        if (!node.targetMappedPort) {
+            return;
+        }
         const targetPort = node.targetMappedPort;
         setExprBarFocusedPort(targetPort);
     };
📝 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.

Suggested change
const onClickEdit = () => {
const targetPort = node.targetMappedPort;
setExprBarFocusedPort(targetPort);
};
const onClickEdit = () => {
if (!node.targetMappedPort) {
return;
}
const targetPort = node.targetMappedPort;
setExprBarFocusedPort(targetPort);
};
🤖 Prompt for AI Agents
In
workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNodeWidget.tsx
around lines 49 to 52, the onClickEdit handler reads node.targetMappedPort
without checking for undefined; update the handler to guard against undefined by
reading targetMappedPort into a local variable, checking if it is truthy (or not
undefined) before calling setExprBarFocusedPort, and return early or no-op when
it's missing so setExprBarFocusedPort never receives an invalid value.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

dtto

Copy link
Contributor

Choose a reason for hiding this comment

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

@KCSAbeywickrama, thanks for confirming! 👍

</div>
</div>
);
} No newline at end of file
Copy link
Member

Choose a reason for hiding this comment

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

Missing the new line at the end of the file. Pls add it in other applicable places as wel

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

Copy link
Contributor

@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

🧹 Nitpick comments (1)
workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNode.ts (1)

74-86: Consider using a Set for more efficient duplicate checking.

The current implementation checks for duplicate ports using .some() inside the loop, resulting in O(n·m) complexity. For better performance with many inputs, consider using a Set to track port IDs.

+        const existingPortIds = new Set<string>();
         this.query.inputs.forEach((field) => {
             const inputField = field?.split('.').pop();
             const matchedSearch = inputSearch === "" || inputField.toLowerCase().includes(inputSearch.toLowerCase());
 
             if (!matchedSearch) return;
 
             const inputNode = findInputNode(field, this, views, lastViewIndex);
             if (inputNode) {
                 const inputPort = getInputPort(inputNode, field?.replace(/\.\d+/g, ''));
-                if (!this.sourcePorts.some(port => port.getID() === inputPort.getID())) {
+                if (!existingPortIds.has(inputPort.getID())) {
                     this.sourcePorts.push(inputPort);
+                    existingPortIds.add(inputPort.getID());
                 }
             }
         });
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f6a978a and 880cbd0.

📒 Files selected for processing (4)
  • workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNode.ts (1 hunks)
  • workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNodeFactory.tsx (1 hunks)
  • workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNodeWidget.tsx (1 hunks)
  • workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/index.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/index.ts
  • workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNodeFactory.tsx
  • workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNodeWidget.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNode.ts (8)
workspaces/ballerina/data-mapper/src/components/Diagram/Port/model/InputOutputPortModel.ts (1)
  • InputOutputPortModel (54-123)
workspaces/ballerina/ballerina-core/src/interfaces/data-mapper.ts (1)
  • DMDiagnostic (70-83)
workspaces/ballerina/data-mapper/src/utils/DataMapperContext/DataMapperContext.ts (1)
  • IDataMapperContext (21-35)
workspaces/ballerina/data-mapper/src/store/store.ts (1)
  • useDMSearchStore (83-89)
workspaces/ballerina/data-mapper/src/components/Diagram/utils/node-utils.ts (1)
  • findInputNode (23-47)
workspaces/ballerina/data-mapper/src/components/Diagram/utils/port-utils.ts (2)
  • getInputPort (26-37)
  • getTargetPortPrefix (60-73)
workspaces/ballerina/data-mapper/src/components/Diagram/Node/QueryOutput/QueryOutputNode.ts (1)
  • QueryOutputNode (37-186)
workspaces/ballerina/data-mapper/src/components/Diagram/Link/DataMapperLink/DataMapperLink.ts (1)
  • DataMapperLinkModel (37-95)
🔇 Additional comments (3)
workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNode.ts (3)

18-28: LGTM!

All imports are properly used throughout the implementation.


109-167: LGTM!

The link creation logic correctly handles port connections and event propagation. The concerns about targetMappedPort potentially being undefined were previously discussed, and the fail-fast error handling approach via ErrorBoundary is an architectural decision that has been clarified.


169-183: LGTM!

Both updatePosition and hasError methods are implemented correctly with appropriate guards and optional chaining.

export class ClauseConnectorNode extends DataMapperNodeModel {

public sourcePorts: InputOutputPortModel[] = [];
public targetMappedPort: InputOutputPortModel;
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Add undefined to the type union for targetMappedPort.

The property is set to undefined at line 62 and is only conditionally assigned (lines 92-106), but the type declaration doesn't reflect this. This bypasses TypeScript's null-safety checks.

Apply this diff:

-    public targetMappedPort: InputOutputPortModel;
+    public targetMappedPort: InputOutputPortModel | undefined;

This will ensure TypeScript enforces proper null checks wherever targetMappedPort is accessed.

🤖 Prompt for AI Agents
In
workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNode.ts
around line 36, the property declaration for targetMappedPort currently omits
undefined even though it's assigned undefined at line 62 and only conditionally
set later (lines 92–106); update the type declaration to include undefined in
the union (e.g., InputOutputPortModel | undefined) so TypeScript enforces
null-safety, then run typecheck and add any necessary null/undefined guards
where targetMappedPort is accessed.

@KCSAbeywickrama KCSAbeywickrama merged commit c70be34 into wso2:main Nov 20, 2025
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Frontend changes to add support for the join clause

2 participants