Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
8a57d5d
Add ClauseConnectorNode, factory, widget, and index files
KCSAbeywickrama Nov 2, 2025
e0ab1c9
Register ClauseConnectorNodeFactory in the diagram engine and export …
KCSAbeywickrama Nov 2, 2025
91e06a1
Refactor ClauseConnectorNode to use Query and update related components
KCSAbeywickrama Nov 2, 2025
7d6492d
Add ClauseConnectorNode support in IONodesScrollCanvasAction and posi…
KCSAbeywickrama Nov 3, 2025
15f0c9e
Add focus link support for ClauseConnector
KCSAbeywickrama Nov 3, 2025
5391f85
Add JOIN clause to IntermediateClauseType and extend IntermediateClau…
KCSAbeywickrama Nov 3, 2025
1c77867
Remove focusedMemberId handling from IOType and refactor InputNode an…
KCSAbeywickrama Nov 3, 2025
6ae1e87
Refactor ExpressionLabelFactory and ExpressionLabelModel to remove Qu…
KCSAbeywickrama Nov 3, 2025
1e00af8
Support focused view with multiple source fields
KCSAbeywickrama Nov 4, 2025
91a02ba
Enhance clause support by adding JOIN clause support and updating mod…
KCSAbeywickrama Nov 4, 2025
f365913
Reposition hasInputsOutputsChanged in DataMapperContext constructor a…
KCSAbeywickrama Nov 6, 2025
b1f92db
Add clauseToAdd prop to DataMapperQueryClausesPanelStore
KCSAbeywickrama Nov 6, 2025
2d5c990
Enhance query clause handling by supporting array join
KCSAbeywickrama Nov 6, 2025
aef51f4
Refactor link creation logic to remove isQueryHeaderPort checks for I…
KCSAbeywickrama Nov 6, 2025
75b1275
Enhance link creation logic for ClauseConnectorNode in CreateLinkState
KCSAbeywickrama Nov 6, 2025
fc3a180
Update label and documentation for isOuter field to clarify it as a l…
KCSAbeywickrama Nov 6, 2025
22a322c
Add mapWithJoin function to handle JOIN clause creation
KCSAbeywickrama Nov 10, 2025
7f337ff
Merge branch 'main' of github.com:wso2/vscode-extensions into bi-dm-j…
KCSAbeywickrama Nov 10, 2025
0f3b842
Merge branch 'main' of github.com:wso2/vscode-extensions into bi-dm-j…
KCSAbeywickrama Nov 12, 2025
5949afc
Refactor ClauseEditor to enhance JOIN clause handling and update fiel…
KCSAbeywickrama Nov 12, 2025
75d4c7b
Merge branch 'main' into bi-dm-join-clause
madushajg Nov 20, 2025
d972369
Improve documentation clarity, optimize imports, and enhance node uti…
KCSAbeywickrama Nov 20, 2025
f6a978a
Merge branch 'bi-dm-join-clause' of github.com:KCSAbeywickrama/vscode…
KCSAbeywickrama Nov 20, 2025
420e998
Add missing new lines at the end of files in ClauseConnector
KCSAbeywickrama Nov 20, 2025
880cbd0
Merge branch 'main' of github.com:wso2/vscode-extensions into bi-dm-j…
KCSAbeywickrama Nov 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ export enum IntermediateClauseType {
WHERE = "where",
FROM = "from",
ORDER_BY = "order by",
LIMIT = "limit"
LIMIT = "limit",
JOIN = "join",
}

export enum ResultClauseType {
Expand Down Expand Up @@ -93,7 +94,6 @@ export interface IOType {
members?: IOType[];
defaultValue?: unknown;
optional?: boolean;
focusedMemberId?: string;
isFocused?: boolean;
isRecursive?: boolean;
isDeepNested?: boolean;
Expand Down Expand Up @@ -125,6 +125,7 @@ export interface ExpandedDMModel {
query?: Query;
mapping_fields?: Record<string, any>;
triggerRefresh?: boolean;
focusInputRootMap?: Record<string, string>;
}

export interface DMModel {
Expand All @@ -138,6 +139,8 @@ export interface DMModel {
focusInputs?: Record<string, IOTypeField>;
mapping_fields?: Record<string, any>;
triggerRefresh?: boolean;
traversingRoot?: string;
focusInputRootMap?: Record<string, string>;
}

export interface ModelState {
Expand Down Expand Up @@ -205,6 +208,9 @@ export interface IntermediateClauseProps {
type?: string;
expression: string;
order?: "ascending" | "descending";
lhsExpression?: string;
rhsExpression?: string;
isOuter?: boolean;
}

export interface IntermediateClause {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,8 @@ export function expandDMModel(
query: model.query,
source: "",
rootViewId,
triggerRefresh: model.triggerRefresh
triggerRefresh: model.triggerRefresh,
focusInputRootMap: model.focusInputRootMap
};
}

Expand All @@ -485,13 +486,18 @@ function processInputRoots(model: DMModel): IOType[] {
inputs.push(input);
}
}

model.focusInputRootMap = {};
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 focusInputRootMap is initialized on the model object without checking if it already exists, which could overwrite existing data. Consider checking first:

if (!model.focusInputRootMap) {
    model.focusInputRootMap = {};
}
Suggested change
model.focusInputRootMap = {};
if (!model.focusInputRootMap) {
model.focusInputRootMap = {};
}

Copilot uses AI. Check for mistakes.
const preProcessedModel: DMModel = {
...model,
inputs,
focusInputs
};

return inputs.map(input => processIORoot(input, preProcessedModel));
return inputs.map(input => {
preProcessedModel.traversingRoot = input.name;
return processIORoot(input, preProcessedModel);
});
}

/**
Expand Down Expand Up @@ -599,6 +605,10 @@ function processArray(
parentId = member.name;
fieldId = member.name;
isFocused = true;

if (model.traversingRoot){
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 checks if (model.traversingRoot) but traversingRoot could be an empty string, which would be falsy. If an empty string is a valid value, use a more explicit check:

if (model.traversingRoot !== undefined && model.traversingRoot !== null) {
    model.focusInputRootMap[parentId] = model.traversingRoot;
}
Suggested change
if (model.traversingRoot){
if (model.traversingRoot !== undefined && model.traversingRoot !== null){

Copilot uses AI. Check for mistakes.
model.focusInputRootMap[parentId] = model.traversingRoot;
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -714,7 +714,7 @@ export function DataMapperView(props: DataMapperProps) {
};

const getModelSignature = (model: DMModel | ExpandedDMModel): ModelSignature => ({
inputs: model.inputs.map(i => i.name),
inputs: [...model.inputs.map(i => i.name), ...(model.query?.inputs || [])],
output: model.output.name,
subMappings: model.subMappings?.map(s => (s as IORoot | IOType).name) || [],
refs: 'refs' in model ? JSON.stringify(model.refs) : ''
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ export function DataMapperEditor(props: DataMapperEditorProps) {
hasInputsOutputsChanged = false
} = modelState;

const initialView = [{
const initialView: View[] = [{
label: model.output.name,
targetField: name
}];
Expand Down Expand Up @@ -228,10 +228,10 @@ export function DataMapperEditor(props: DataMapperEditorProps) {
const context = new DataMapperContext(
model,
views,
hasInputsOutputsChanged,
addView,
applyModifications,
addArrayElement,
hasInputsOutputsChanged,
convertToQuery,
deleteMapping,
deleteSubMapping,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import React from "react";
import { EditorContainer } from "./styles";
import { Divider, Dropdown, OptionProps, Typography } from "@wso2/ui-toolkit";
import { DMFormProps, DMFormField, DMFormFieldValues, IntermediateClauseType, IntermediateClause, IntermediateClauseProps } from "@wso2/ballerina-core";
import { useDMQueryClausesPanelStore } from "../../../../store/store";

export interface ClauseEditorProps {
clause?: IntermediateClause;
Expand All @@ -32,24 +33,26 @@ export interface ClauseEditorProps {

export function ClauseEditor(props: ClauseEditorProps) {
const { clause, onSubmitText, isSaving, onSubmit, onCancel, generateForm } = props;
const { type: _clauseType, properties: clauseProps } = clause ?? {};
const { clauseToAdd, setClauseToAdd } = useDMQueryClausesPanelStore.getState();
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.

Using getState() directly in the component body will not cause re-renders when the state changes. This should use the hook form useDMQueryClausesPanelStore() to properly subscribe to state updates.

const { clauseToAdd, setClauseToAdd } = useDMQueryClausesPanelStore();
Suggested change
const { clauseToAdd, setClauseToAdd } = useDMQueryClausesPanelStore.getState();
const { clauseToAdd, setClauseToAdd } = useDMQueryClausesPanelStore();

Copilot uses AI. Check for mistakes.
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 setClauseToAdd function is retrieved from getState() instead of the hook, which means it won't have the proper reactive context. This should be moved to use the hook pattern for consistency with line 35.

const { clauseToAdd } = useDMQueryClausesPanelStore();
const setClauseToAdd = useDMQueryClausesPanelStore(state => state.setClauseToAdd);
Suggested change
const { clauseToAdd, setClauseToAdd } = useDMQueryClausesPanelStore.getState();
const { clauseToAdd, setClauseToAdd } = useDMQueryClausesPanelStore();

Copilot uses AI. Check for mistakes.
const { type: _clauseType, properties: clauseProps } = clause ?? clauseToAdd ?? {};

const [clauseType, setClauseType] = React.useState<string>(_clauseType ?? IntermediateClauseType.WHERE);
const clauseTypeItems: OptionProps[] = [
{ content: "condition", value: IntermediateClauseType.WHERE },
{ content: "local variable", value: IntermediateClauseType.LET },
{ content: "sort by", value: IntermediateClauseType.ORDER_BY },
{ content: "limit", value: IntermediateClauseType.LIMIT },
{ content: "from", value: IntermediateClauseType.FROM }
{ content: "from", value: IntermediateClauseType.FROM },
{ content: "join", value: IntermediateClauseType.JOIN },
]

const nameField: DMFormField = {
key: "name",
label: "Name",
label: clauseType === IntermediateClauseType.JOIN ? "Item Alias" : "Name",
type: "IDENTIFIER",
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.
value: clauseProps?.name ?? "",
valueTypeConstraint: "Global",
enabled: true,
Expand All @@ -61,19 +64,19 @@ export function ClauseEditor(props: ClauseEditorProps) {
type: "TYPE",
optional: false,
editable: true,
documentation: "Enter the type of the clause.",
documentation: "Enter the type of the clause",
value: clauseProps?.type ?? "",
valueTypeConstraint: "Global",
enabled: true,
}

const expressionField: DMFormField = {
key: "expression",
label: "Expression",
label: clauseType === IntermediateClauseType.JOIN ? "Join With Collection" : "Expression",
type: "EXPRESSION",
optional: false,
editable: true,
documentation: "Enter the expression of the clause.",
documentation: clauseType === IntermediateClauseType.JOIN ? "Collection to be joined" : "Enter the expression of the clause",
value: clauseProps?.expression ?? "",
valueTypeConstraint: "Global",
enabled: true,
Expand All @@ -85,18 +88,53 @@ export function ClauseEditor(props: ClauseEditorProps) {
type: "ENUM",
optional: false,
editable: true,
documentation: "Enter the order.",
documentation: "Enter the order",
value: clauseProps?.order ?? "",
valueTypeConstraint: "Global",
enabled: true,
items: ["ascending", "descending"]
}

const lhsExpressionField: DMFormField = {
key: "lhsExpression",
label: "LHS Expression",
type: "EXPRESSION",
optional: false,
editable: true,
documentation: "Enter the LHS expression of join-on condition",
value: clauseProps?.lhsExpression ?? "",
valueTypeConstraint: "Global",
enabled: true,
}

const rhsExpressionField: DMFormField = {
key: "rhsExpression",
label: "RHS Expression",
type: "EXPRESSION",
optional: false,
editable: true,
documentation: "Enter the RHS expression of join-on condition",
value: clauseProps?.rhsExpression ?? "",
valueTypeConstraint: "Global",
enabled: true,
}

const handleSubmit = (data: DMFormFieldValues) => {
onSubmit({
setClauseToAdd(undefined);
const clause: IntermediateClause = {
type: clauseType as IntermediateClauseType,
properties: data as IntermediateClauseProps
});
};
if (clauseType === IntermediateClauseType.JOIN) {
clause.properties.type = "var";
clause.properties.isOuter = false;
}
onSubmit(clause);
}

const handleCancel = () => {
setClauseToAdd(undefined);
onCancel();
}

// function with select case to gen fields based on clause type
Expand All @@ -107,6 +145,8 @@ export function ClauseEditor(props: ClauseEditorProps) {
return [nameField, typeField, expressionField];
case IntermediateClauseType.ORDER_BY:
return [expressionField, orderField];
case IntermediateClauseType.JOIN:
return [expressionField, nameField, lhsExpressionField, rhsExpressionField];
default:
return [expressionField];
}
Expand All @@ -119,7 +159,7 @@ export function ClauseEditor(props: ClauseEditorProps) {
cancelText: "Cancel",
nestedForm: true,
onSubmit: handleSubmit,
onCancel,
onCancel: handleCancel,
isSaving
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* under the License.
*/

import React from "react";
import React, { useEffect } from "react";

import { Button, Codicon, SidePanel, SidePanelBody, SidePanelTitleContainer, ThemeColors } from "@wso2/ui-toolkit";
import { useDMQueryClausesPanelStore } from "../../../../store/store";
Expand All @@ -35,6 +35,7 @@ export interface ClausesPanelProps {

export function ClausesPanel(props: ClausesPanelProps) {
const { isQueryClausesPanelOpen, setIsQueryClausesPanelOpen } = useDMQueryClausesPanelStore();
const { clauseToAdd, setClauseToAdd } = useDMQueryClausesPanelStore.getState();
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.

Using getState() in the component body will not trigger re-renders when the state changes. This should use the hook form to properly subscribe to state changes:

const { clauseToAdd, setClauseToAdd } = useDMQueryClausesPanelStore();
Suggested change
const { clauseToAdd, setClauseToAdd } = useDMQueryClausesPanelStore.getState();
const { clauseToAdd, setClauseToAdd } = useDMQueryClausesPanelStore();

Copilot uses AI. Check for mistakes.
const { query, targetField, addClauses, deleteClause, generateForm } = props;

const [adding, setAdding] = React.useState<number>();
Expand Down Expand Up @@ -67,11 +68,20 @@ export function ClausesPanel(props: ClausesPanelProps) {
setEditing(undefined);
}

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

return (
<SidePanel
isOpen={isQueryClausesPanelOpen}
alignment="right"
width={312}
width={400}
overlay={false}
sx={{
fontFamily: "GilmerRegular",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
*/
export interface View {
label: string;
sourceField?: string;
sourceFields?: string[];
targetField: string;
subMappingInfo?: SubMappingInfo;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
isOutputNode
} from "./utils";
import { IO_NODE_DEFAULT_WIDTH, VISUALIZER_PADDING, defaultModelOptions } from "../utils/constants";
import { LinkConnectorNode, QueryExprConnectorNode } from "../Node";
import { ClauseConnectorNode, LinkConnectorNode, QueryExprConnectorNode } from "../Node";

export interface PanAndZoomCanvasActionOptions {
inverseZoom?: boolean;
Expand Down Expand Up @@ -161,7 +161,7 @@ function repositionIntermediateNodes(outputNode: NodeModel) {
if (link instanceof DataMapperLinkModel) {
const sourceNode = link.getSourcePort().getNode();
const targetPortPosition = link.getTargetPort().getPosition();
if (sourceNode instanceof LinkConnectorNode || sourceNode instanceof QueryExprConnectorNode) {
if (sourceNode instanceof LinkConnectorNode || sourceNode instanceof QueryExprConnectorNode || sourceNode instanceof ClauseConnectorNode) {
sourceNode.setPosition(sourceNode.getX(), targetPortPosition.y - 4.5);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ import {
QueryExprConnectorNode,
LinkConnectorNode,
QueryOutputNode,
SubMappingNode
SubMappingNode,
ClauseConnectorNode
} from "../Node";
import { IO_NODE_DEFAULT_WIDTH } from "../utils/constants";
import { DataMapperLinkModel } from "../Link";
Expand All @@ -44,7 +45,8 @@ export const OUTPUT_NODES = [

export const INTERMEDIATE_NODES = [
LinkConnectorNode,
QueryExprConnectorNode
QueryExprConnectorNode,
ClauseConnectorNode
];

export const MIN_VISIBLE_HEIGHT = 68;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import { DataMapperCanvasContainerWidget } from './Canvas/DataMapperCanvasContai
import { DataMapperCanvasWidget } from './Canvas/DataMapperCanvasWidget';
import { DefaultState as LinkState } from './LinkState/DefaultState';
import { DataMapperNodeModel } from './Node/commons/DataMapperNode';
import { LinkConnectorNode, QueryExprConnectorNode } from './Node';
import { ClauseConnectorNode, LinkConnectorNode, QueryExprConnectorNode } from './Node';
import { OverlayLayerFactory } from './OverlayLayer/OverlayLayerFactory';
import { OverriddenLinkLayerFactory } from './OverriddenLinkLayer/LinkLayerFactory';
import { useDiagramModel, useFocusLinkedNodes, useRepositionedNodes } from './hooks';
Expand Down Expand Up @@ -87,6 +87,7 @@ function initDiagramEngine() {
engine.getNodeFactories().registerFactory(new Nodes.QueryOutputNodeFactory());
engine.getNodeFactories().registerFactory(new Nodes.LinkConnectorNodeFactory());
engine.getNodeFactories().registerFactory(new Nodes.QueryExprConnectorNodeFactory());
engine.getNodeFactories().registerFactory(new Nodes.ClauseConnectorNodeFactory());
engine.getNodeFactories().registerFactory(new Nodes.DataImportNodeFactory());
engine.getNodeFactories().registerFactory(new Nodes.EmptyInputsNodeFactory());

Expand Down Expand Up @@ -157,10 +158,10 @@ function DataMapperDiagram(props: DataMapperDiagramProps): React.ReactElement {
if (!isFetching && engine.getModel()) {
const modelNodes = engine.getModel().getNodes();
const nodesToUpdate = modelNodes.filter(node =>
node instanceof LinkConnectorNode || node instanceof QueryExprConnectorNode
node instanceof LinkConnectorNode || node instanceof QueryExprConnectorNode || node instanceof ClauseConnectorNode
);

nodesToUpdate.forEach((node: LinkConnectorNode | QueryExprConnectorNode) => {
nodesToUpdate.forEach((node: LinkConnectorNode | QueryExprConnectorNode | ClauseConnectorNode) => {
const targetPortPosition = node.targetMappedPort?.getPosition();
if (targetPortPosition) {
node.setPosition(targetPortPosition.x - 155, targetPortPosition.y - 6.5);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ import { DiagramEngine } from '@projectstorm/react-diagrams';

import { ExpressionLabelModel } from './ExpressionLabelModel';
import { ExpressionLabelWidget } from './ExpressionLabelWidget';
import { QueryExprLabelWidget } from './QueryExprLabelWidget';
import { MappingOptionsWidget } from './MappingOptionsWidget';
import { MappingType } from '../Link';

export class ExpressionLabelFactory extends AbstractReactFactory<ExpressionLabelModel, DiagramEngine> {
constructor() {
Expand All @@ -35,10 +35,10 @@ export class ExpressionLabelFactory extends AbstractReactFactory<ExpressionLabel
}

generateReactWidget(event: GenerateWidgetEvent<ExpressionLabelModel>): JSX.Element {
if (event.model.isQuery) {
return <QueryExprLabelWidget model={event.model} />;
}
if (event.model.link?.pendingMappingType) {
if(event.model.link.pendingMappingType === MappingType.ArrayJoin) {
return <></>;
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.

Returning an empty fragment <></> for ArrayJoin mapping type silently hides the label. Consider adding a comment explaining why no label is needed for JOIN mappings, or use null to be more explicit:

if(event.model.link.pendingMappingType === MappingType.ArrayJoin) {
    // JOIN clause mappings don't require a label as they use ClauseConnectorNode
    return null;
}
Suggested change
return <></>;
// JOIN clause mappings don't require a label as they use ClauseConnectorNode
return null;

Copilot uses AI. Check for mistakes.
}
return <MappingOptionsWidget model={event.model} />;
}
return <ExpressionLabelWidget model={event.model} engine={this.engine} />;
Expand Down
Loading
Loading