From 8a57d5dadbbfd922450682512310890e48663a60 Mon Sep 17 00:00:00 2001 From: ChamodA Date: Sun, 2 Nov 2025 19:43:53 +0530 Subject: [PATCH 01/20] Add ClauseConnectorNode, factory, widget, and index files --- .../ClauseConnector/ClauseConnectorNode.ts | 228 ++++++++++++++++++ .../ClauseConnectorNodeFactory.tsx | 43 ++++ .../ClauseConnectorNodeWidget.tsx | 116 +++++++++ .../Diagram/Node/ClauseConnector/index.ts | 20 ++ 4 files changed, 407 insertions(+) create mode 100644 workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNode.ts create mode 100644 workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNodeFactory.tsx create mode 100644 workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNodeWidget.tsx create mode 100644 workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/index.ts diff --git a/workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNode.ts b/workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNode.ts new file mode 100644 index 00000000000..5b6941a9d20 --- /dev/null +++ b/workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNode.ts @@ -0,0 +1,228 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { DMDiagnostic, Mapping } from "@wso2/ballerina-core"; + +import { IDataMapperContext } from "../../../../utils/DataMapperContext/DataMapperContext"; +import { DataMapperLinkModel } from "../../Link"; +import { InputOutputPortModel, IntermediatePortModel } from "../../Port"; +import { DataMapperNodeModel } from "../commons/DataMapperNode"; +import { ArrayOutputNode } from "../ArrayOutput"; +import { ObjectOutputNode } from "../ObjectOutput"; +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"; +import { QueryOutputNode } from "../QueryOutput"; +import { useDMSearchStore } from "../../../../store/store"; + +export const CLAUSE_CONNECTOR_NODE_TYPE = "clause-connector-node"; +const NODE_ID = "clause-connector-node"; + +export class ClauseConnectorNode extends DataMapperNodeModel { + + public sourcePorts: InputOutputPortModel[] = []; + public targetPort: InputOutputPortModel; + public targetMappedPort: InputOutputPortModel; + + public inPort: IntermediatePortModel; + public outPort: IntermediatePortModel; + + public diagnostics: DMDiagnostic[]; + public value: string; + public hidden: boolean; + public shouldInitLinks: boolean; + public label: string; + + constructor( + public context: IDataMapperContext, + public mapping: Mapping + ) { + super( + NODE_ID, + context, + CLAUSE_CONNECTOR_NODE_TYPE + ); + this.value = mapping.expression; + this.diagnostics = mapping.diagnostics; + } + + initPorts(): void { + const prevSourcePorts = this.sourcePorts; + this.sourcePorts = []; + this.targetMappedPort = undefined; + this.inPort = new IntermediatePortModel(`${this.mapping.inputs.join('_')}_${this.mapping.output}_IN`, "IN"); + this.outPort = new IntermediatePortModel(`${this.mapping.inputs.join('_')}_${this.mapping.output}_OUT`, "OUT"); + this.addPort(this.inPort); + this.addPort(this.outPort); + + const inputSearch = useDMSearchStore.getState().inputSearch; + const outputSearch = useDMSearchStore.getState().outputSearch; + + const views = this.context.views; + const lastViewIndex = views.length - 1; + + this.mapping.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())) { + this.sourcePorts.push(inputPort); + } + } + }) + + const outputField = this.mapping.output.split(".").pop(); + const matchedSearch = outputSearch === "" || outputField.toLowerCase().includes(outputSearch.toLowerCase()); + + if (matchedSearch && this.outPort) { + this.getModel().getNodes().map((node) => { + + if (node instanceof ObjectOutputNode || node instanceof ArrayOutputNode || node instanceof QueryOutputNode) { + const targetPortPrefix = getTargetPortPrefix(node); + + this.targetPort = node.getPort(`${targetPortPrefix}.${this.mapping.output}.IN`) as InputOutputPortModel; + this.targetMappedPort = this.targetPort; + + [this.targetPort, this.targetMappedPort] = getOutputPort(node, this.mapping.output); + const previouslyHidden = this.hidden; + this.hidden = this.targetMappedPort?.attributes.portName !== this.targetPort?.attributes.portName; + if (this.hidden !== previouslyHidden + || (prevSourcePorts.length !== this.sourcePorts.length + || prevSourcePorts.map(port => port.getID()).join('') + !== this.sourcePorts.map(port => port.getID()).join(''))) + { + this.shouldInitLinks = true; + } + } + }); + } + } + + initLinks(): void { + if (!this.shouldInitLinks) { + return; + } + if (this.hidden) { + if (this.targetMappedPort) { + this.sourcePorts.forEach((sourcePort) => { + const inPort = this.targetMappedPort; + const lm = new DataMapperLinkModel(undefined, this.diagnostics, true); + + sourcePort.addLinkedPort(this.targetMappedPort); + + lm.setTargetPort(this.targetMappedPort); + 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 as any); + + if (!this.label) { + this.label = this.targetMappedPort.attributes.fieldFQN.split('.').pop(); + } + }) + } + } else { + this.sourcePorts.forEach((sourcePort) => { + const inPort = this.inPort; + const lm = new DataMapperLinkModel(undefined, this.diagnostics, 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 as any); + } + }) + + if (this.targetMappedPort) { + const outPort = this.outPort; + const targetPort = this.targetMappedPort; + + const lm = new DataMapperLinkModel(undefined, this.diagnostics, true); + + lm.setTargetPort(this.targetMappedPort); + lm.setSourcePort(this.outPort); + lm.registerListener({ + selectionChanged(event) { + if (event.isSelected) { + outPort.fireEvent({}, "link-selected"); + targetPort.fireEvent({}, "link-selected"); + } else { + outPort.fireEvent({}, "link-unselected"); + targetPort.fireEvent({}, "link-unselected"); + } + }, + }) + + if (!this.label) { + const fieldFQN = this.targetMappedPort.attributes.fieldFQN; + this.label = fieldFQN ? this.targetMappedPort.attributes.fieldFQN.split('.').pop() : ''; + } + this.getModel().addAll(lm as any); + } + } + this.shouldInitLinks = false; + } + + public updatePosition() { + if (this.targetMappedPort) { + const position = this.targetMappedPort.getPosition(); + this.setPosition( + this.hasError() + ? OFFSETS.LINK_CONNECTOR_NODE_WITH_ERROR.X + : OFFSETS.LINK_CONNECTOR_NODE.X, + position.y - 2 + ); + } + } + + public async deleteLink(): Promise { + await removeMapping(this.mapping, this.context); + } + + public hasError(): boolean { + return this.diagnostics?.length > 0; + } +} \ No newline at end of file diff --git a/workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNodeFactory.tsx b/workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNodeFactory.tsx new file mode 100644 index 00000000000..e2ae6becd22 --- /dev/null +++ b/workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNodeFactory.tsx @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import * as React from 'react'; + +import { AbstractReactFactory } from '@projectstorm/react-canvas-core'; +import { DiagramEngine } from '@projectstorm/react-diagrams-core'; + +import { ClauseConnectorNode, CLAUSE_CONNECTOR_NODE_TYPE } from './ClauseConnectorNode'; +import { ClauseConnectorNodeWidget } from './ClauseConnectorNodeWidget'; + +export class ClauseConnectorNodeFactory extends AbstractReactFactory { + constructor() { + super(CLAUSE_CONNECTOR_NODE_TYPE); + } + + generateReactWidget(event: { model: ClauseConnectorNode; }): JSX.Element { + const inputPortHasLinks = Object.keys(event.model.inPort.links).length; + const outputPortHasLinks = Object.keys(event.model.outPort.links).length; + if (inputPortHasLinks && outputPortHasLinks) { + return ; + } + return null; + } + + generateModel(): ClauseConnectorNode { + return undefined; + } +} \ No newline at end of file diff --git a/workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNodeWidget.tsx b/workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNodeWidget.tsx new file mode 100644 index 00000000000..11578b960d9 --- /dev/null +++ b/workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNodeWidget.tsx @@ -0,0 +1,116 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +// tslint:disable: jsx-no-multiline-js +import React, { useState } from "react"; +import { DiagramEngine } from '@projectstorm/react-diagrams-core'; +import { Button, Codicon, ProgressRing } from '@wso2/ui-toolkit'; +import classnames from 'classnames'; + +import { useIntermediateNodeStyles } from '../../../styles'; +import { ClauseConnectorNode } from './ClauseConnectorNode'; +import { renderDeleteButton, renderEditButton, renderPortWidget } from "../LinkConnector/LinkConnectorWidgetComponents"; +import { DiagnosticWidget } from "../../Diagnostic/DiagnosticWidget"; +import { expandArrayFn } from "../../utils/common-utils"; +import { useDMCollapsedFieldsStore, useDMExpandedFieldsStore, useDMExpressionBarStore } from "../../../../store/store"; + +export interface ClauseConnectorNodeWidgetProps { + node: ClauseConnectorNode; + engine: DiagramEngine; +} + +export function ClauseConnectorNodeWidget(props: ClauseConnectorNodeWidgetProps) { + const { node, engine } = props; + + const classes = useIntermediateNodeStyles(); + const setExprBarFocusedPort = useDMExpressionBarStore(state => state.setFocusedPort); + const collapsedFieldsStore = useDMCollapsedFieldsStore(); + const expandedFieldsStore = useDMExpandedFieldsStore(); + + const diagnostic = node.hasError() ? node.diagnostics[0] : null; + const value = node.value; + + const [deleteInProgress, setDeleteInProgress] = useState(false); + + const onClickEdit = () => { + const targetPort = node.targetMappedPort; + setExprBarFocusedPort(targetPort); + }; + + const onClickDelete = async () => { + setDeleteInProgress(true); + if (node.deleteLink) { + await node.deleteLink(); + } + setDeleteInProgress(false); + }; + + const onFocusClause = () => { + const sourcePorts = node.sourcePorts.map(port => port.attributes.portName); + const targetPort = node.targetMappedPort.attributes.portName; + + sourcePorts.forEach((port) => { + collapsedFieldsStore.removeField(port); + expandedFieldsStore.removeField(port); + }); + collapsedFieldsStore.removeField(targetPort); + expandedFieldsStore.removeField(targetPort); + + const context = node.context; + const lastView = context.views[context.views.length - 1]; + const mapping = node.targetMappedPort.attributes.value; + expandArrayFn(context, mapping.inputs[0], mapping.output, lastView.targetField); + }; + + const loadingScreen = ( +
+ +
+ ); + + return (!node.hidden && ( +
+
+ {renderPortWidget(engine, node.inPort, `${node?.value}-input`)} + {renderEditButton(onClickEdit, node?.value)} + + {deleteInProgress ? ( + loadingScreen + ) : ( + <>{renderDeleteButton(onClickDelete, node?.value)} + )} + {diagnostic && ( + + )} + {renderPortWidget(engine, node.outPort, `${node?.value}-output`)} +
+
+ ) + ); +} \ No newline at end of file diff --git a/workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/index.ts b/workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/index.ts new file mode 100644 index 00000000000..8c732f99edb --- /dev/null +++ b/workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/index.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +export * from "./ClauseConnectorNode"; +export * from "./ClauseConnectorNodeFactory"; +export * from "./ClauseConnectorNodeWidget"; \ No newline at end of file From e0ab1c92fdeb575609dd8da35d09dfc2cd6f81e5 Mon Sep 17 00:00:00 2001 From: ChamodA Date: Sun, 2 Nov 2025 19:46:29 +0530 Subject: [PATCH 02/20] Register ClauseConnectorNodeFactory in the diagram engine and export ClauseConnector from the node index --- .../ballerina/data-mapper/src/components/Diagram/Diagram.tsx | 1 + .../ballerina/data-mapper/src/components/Diagram/Node/index.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/workspaces/ballerina/data-mapper/src/components/Diagram/Diagram.tsx b/workspaces/ballerina/data-mapper/src/components/Diagram/Diagram.tsx index 542a0886935..63d6cd763f6 100644 --- a/workspaces/ballerina/data-mapper/src/components/Diagram/Diagram.tsx +++ b/workspaces/ballerina/data-mapper/src/components/Diagram/Diagram.tsx @@ -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()); diff --git a/workspaces/ballerina/data-mapper/src/components/Diagram/Node/index.ts b/workspaces/ballerina/data-mapper/src/components/Diagram/Node/index.ts index 8da41fe9613..2aa01e08ba9 100644 --- a/workspaces/ballerina/data-mapper/src/components/Diagram/Node/index.ts +++ b/workspaces/ballerina/data-mapper/src/components/Diagram/Node/index.ts @@ -25,4 +25,5 @@ export * from "./PrimitiveOutput"; export * from "./QueryOutput"; export * from "./LinkConnector"; export * from "./QueryExprConnector"; +export * from "./ClauseConnector"; export * from "./EmptyInputs"; From 91e06a171945e5ff11b1be522674153809a08d59 Mon Sep 17 00:00:00 2001 From: ChamodA Date: Sun, 2 Nov 2025 21:42:38 +0530 Subject: [PATCH 03/20] Refactor ClauseConnectorNode to use Query and update related components --- .../ClauseConnector/ClauseConnectorNode.ts | 153 +++++++----------- .../ClauseConnectorNodeWidget.tsx | 27 +--- .../Node/QueryOutput/QueryOutputNode.ts | 34 ++-- .../data-mapper/src/utils/model-utils.ts | 12 +- .../data-mapper/src/visitors/BaseVisitor.ts | 6 +- .../visitors/IntermediateNodeInitVisitor.ts | 10 +- 6 files changed, 102 insertions(+), 140 deletions(-) diff --git a/workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNode.ts b/workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNode.ts index 5b6941a9d20..25d26bdaf74 100644 --- a/workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNode.ts +++ b/workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNode.ts @@ -15,7 +15,7 @@ * specific language governing permissions and limitations * under the License. */ -import { DMDiagnostic, Mapping } from "@wso2/ballerina-core"; +import { DMDiagnostic, Query } from "@wso2/ballerina-core"; import { IDataMapperContext } from "../../../../utils/DataMapperContext/DataMapperContext"; import { DataMapperLinkModel } from "../../Link"; @@ -37,36 +37,34 @@ export class ClauseConnectorNode extends DataMapperNodeModel { public sourcePorts: InputOutputPortModel[] = []; public targetPort: InputOutputPortModel; - public targetMappedPort: InputOutputPortModel; public inPort: IntermediatePortModel; public outPort: IntermediatePortModel; public diagnostics: DMDiagnostic[]; public value: string; - public hidden: boolean; public shouldInitLinks: boolean; public label: string; constructor( public context: IDataMapperContext, - public mapping: Mapping + public query: Query ) { super( NODE_ID, context, CLAUSE_CONNECTOR_NODE_TYPE ); - this.value = mapping.expression; - this.diagnostics = mapping.diagnostics; + this.value = query.output; + this.diagnostics = query.diagnostics; } initPorts(): void { const prevSourcePorts = this.sourcePorts; this.sourcePorts = []; - this.targetMappedPort = undefined; - this.inPort = new IntermediatePortModel(`${this.mapping.inputs.join('_')}_${this.mapping.output}_IN`, "IN"); - this.outPort = new IntermediatePortModel(`${this.mapping.inputs.join('_')}_${this.mapping.output}_OUT`, "OUT"); + this.targetPort = undefined; + this.inPort = new IntermediatePortModel(`${this.query.inputs.join('_')}_${this.query.output}_IN`, "IN"); + this.outPort = new IntermediatePortModel(`${this.query.inputs.join('_')}_${this.query.output}_OUT`, "OUT"); this.addPort(this.inPort); this.addPort(this.outPort); @@ -76,7 +74,7 @@ export class ClauseConnectorNode extends DataMapperNodeModel { const views = this.context.views; const lastViewIndex = views.length - 1; - this.mapping.inputs.forEach((field) => { + this.query.inputs.forEach((field) => { const inputField = field?.split('.').pop(); const matchedSearch = inputSearch === "" || inputField.toLowerCase().includes(inputSearch.toLowerCase()); @@ -91,26 +89,19 @@ export class ClauseConnectorNode extends DataMapperNodeModel { } }) - const outputField = this.mapping.output.split(".").pop(); + const outputField = this.query.output.split(".").pop(); const matchedSearch = outputSearch === "" || outputField.toLowerCase().includes(outputSearch.toLowerCase()); if (matchedSearch && this.outPort) { this.getModel().getNodes().map((node) => { - if (node instanceof ObjectOutputNode || node instanceof ArrayOutputNode || node instanceof QueryOutputNode) { + if (node instanceof QueryOutputNode) { const targetPortPrefix = getTargetPortPrefix(node); - this.targetPort = node.getPort(`${targetPortPrefix}.${this.mapping.output}.IN`) as InputOutputPortModel; - this.targetMappedPort = this.targetPort; - - [this.targetPort, this.targetMappedPort] = getOutputPort(node, this.mapping.output); - const previouslyHidden = this.hidden; - this.hidden = this.targetMappedPort?.attributes.portName !== this.targetPort?.attributes.portName; - if (this.hidden !== previouslyHidden - || (prevSourcePorts.length !== this.sourcePorts.length - || prevSourcePorts.map(port => port.getID()).join('') - !== this.sourcePorts.map(port => port.getID()).join(''))) - { + this.targetPort = node.getPort(`${targetPortPrefix}.${this.query.output}.#.IN`) as InputOutputPortModel; + + if (prevSourcePorts.length !== this.sourcePorts.length || + prevSourcePorts.map(port => port.getID()).join('') !== this.sourcePorts.map(port => port.getID()).join('')) { this.shouldInitLinks = true; } } @@ -122,93 +113,65 @@ export class ClauseConnectorNode extends DataMapperNodeModel { if (!this.shouldInitLinks) { return; } - if (this.hidden) { - if (this.targetMappedPort) { - this.sourcePorts.forEach((sourcePort) => { - const inPort = this.targetMappedPort; - const lm = new DataMapperLinkModel(undefined, this.diagnostics, true); - - sourcePort.addLinkedPort(this.targetMappedPort); - - lm.setTargetPort(this.targetMappedPort); - 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 as any); - - if (!this.label) { - this.label = this.targetMappedPort.attributes.fieldFQN.split('.').pop(); - } - }) - } - } else { - this.sourcePorts.forEach((sourcePort) => { - const inPort = this.inPort; - const lm = new DataMapperLinkModel(undefined, this.diagnostics, 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 as any); - } - }) - if (this.targetMappedPort) { - const outPort = this.outPort; - const targetPort = this.targetMappedPort; + this.sourcePorts.forEach((sourcePort) => { + const inPort = this.inPort; + const lm = new DataMapperLinkModel(undefined, this.diagnostics, true, undefined, true); - const lm = new DataMapperLinkModel(undefined, this.diagnostics, true); + if (sourcePort) { + sourcePort.addLinkedPort(this.inPort); + sourcePort.addLinkedPort(this.targetPort) - lm.setTargetPort(this.targetMappedPort); - lm.setSourcePort(this.outPort); + lm.setTargetPort(this.inPort); + lm.setSourcePort(sourcePort); lm.registerListener({ selectionChanged(event) { if (event.isSelected) { - outPort.fireEvent({}, "link-selected"); - targetPort.fireEvent({}, "link-selected"); + inPort.fireEvent({}, "link-selected"); + sourcePort.fireEvent({}, "link-selected"); } else { - outPort.fireEvent({}, "link-unselected"); - targetPort.fireEvent({}, "link-unselected"); + inPort.fireEvent({}, "link-unselected"); + sourcePort.fireEvent({}, "link-unselected"); } }, }) - - if (!this.label) { - const fieldFQN = this.targetMappedPort.attributes.fieldFQN; - this.label = fieldFQN ? this.targetMappedPort.attributes.fieldFQN.split('.').pop() : ''; - } this.getModel().addAll(lm as any); } + }) + + if (this.targetPort) { + const outPort = this.outPort; + const targetPort = this.targetPort; + + const lm = new DataMapperLinkModel(undefined, this.diagnostics, true, undefined, true); + + lm.setTargetPort(this.targetPort); + lm.setSourcePort(this.outPort); + lm.registerListener({ + selectionChanged(event) { + if (event.isSelected) { + outPort.fireEvent({}, "link-selected"); + targetPort.fireEvent({}, "link-selected"); + } else { + outPort.fireEvent({}, "link-unselected"); + targetPort.fireEvent({}, "link-unselected"); + } + }, + }) + + if (!this.label) { + const fieldFQN = this.targetPort.attributes.fieldFQN; + this.label = fieldFQN ? this.targetPort.attributes.fieldFQN.split('.').pop() : ''; + } + this.getModel().addAll(lm as any); } + this.shouldInitLinks = false; } public updatePosition() { - if (this.targetMappedPort) { - const position = this.targetMappedPort.getPosition(); + if (this.targetPort) { + const position = this.targetPort.getPosition(); this.setPosition( this.hasError() ? OFFSETS.LINK_CONNECTOR_NODE_WITH_ERROR.X @@ -218,10 +181,6 @@ export class ClauseConnectorNode extends DataMapperNodeModel { } } - public async deleteLink(): Promise { - await removeMapping(this.mapping, this.context); - } - public hasError(): boolean { return this.diagnostics?.length > 0; } diff --git a/workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNodeWidget.tsx b/workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNodeWidget.tsx index 11578b960d9..2f97be030c9 100644 --- a/workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNodeWidget.tsx +++ b/workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNodeWidget.tsx @@ -47,21 +47,13 @@ export function ClauseConnectorNodeWidget(props: ClauseConnectorNodeWidgetProps) const [deleteInProgress, setDeleteInProgress] = useState(false); const onClickEdit = () => { - const targetPort = node.targetMappedPort; + const targetPort = node.targetPort; setExprBarFocusedPort(targetPort); }; - const onClickDelete = async () => { - setDeleteInProgress(true); - if (node.deleteLink) { - await node.deleteLink(); - } - setDeleteInProgress(false); - }; - const onFocusClause = () => { const sourcePorts = node.sourcePorts.map(port => port.attributes.portName); - const targetPort = node.targetMappedPort.attributes.portName; + const targetPort = node.targetPort.attributes.portName; sourcePorts.forEach((port) => { collapsedFieldsStore.removeField(port); @@ -72,7 +64,7 @@ export function ClauseConnectorNodeWidget(props: ClauseConnectorNodeWidgetProps) const context = node.context; const lastView = context.views[context.views.length - 1]; - const mapping = node.targetMappedPort.attributes.value; + const mapping = node.targetPort.attributes.value; expandArrayFn(context, mapping.inputs[0], mapping.output, lastView.targetField); }; @@ -82,24 +74,18 @@ export function ClauseConnectorNodeWidget(props: ClauseConnectorNodeWidgetProps) ); - return (!node.hidden && ( + return (
{renderPortWidget(engine, node.inPort, `${node?.value}-input`)} - {renderEditButton(onClickEdit, node?.value)} - {deleteInProgress ? ( - loadingScreen - ) : ( - <>{renderDeleteButton(onClickDelete, node?.value)} - )} {diagnostic && (
- ) ); } \ No newline at end of file diff --git a/workspaces/ballerina/data-mapper/src/components/Diagram/Node/QueryOutput/QueryOutputNode.ts b/workspaces/ballerina/data-mapper/src/components/Diagram/Node/QueryOutput/QueryOutputNode.ts index 83d43bc2d2d..a3203d16f8e 100644 --- a/workspaces/ballerina/data-mapper/src/components/Diagram/Node/QueryOutput/QueryOutputNode.ts +++ b/workspaces/ballerina/data-mapper/src/components/Diagram/Node/QueryOutput/QueryOutputNode.ts @@ -115,7 +115,7 @@ export class QueryOutputNode extends DataMapperNodeModel { const views = this.context.views; const lastViewIndex = views.length - 1; - const { inputs: queryInputs, output: queryOutput} = this.context.model.query; + // const { inputs: queryInputs, output: queryOutput} = this.context.model.query; mappings.forEach((mapping) => { @@ -164,27 +164,27 @@ export class QueryOutputNode extends DataMapperNodeModel { } }); - const inputNode = findInputNode(queryInputs[0], this, views, lastViewIndex); - let inPort: InputOutputPortModel; - if (inputNode) { - inPort = getInputPort(inputNode, queryInputs[0].replace(/\.\d+/g, '')); - } + // const inputNode = findInputNode(queryInputs[0], this, views, lastViewIndex); + // let inPort: InputOutputPortModel; + // if (inputNode) { + // inPort = getInputPort(inputNode, queryInputs[0].replace(/\.\d+/g, '')); + // } - const [_, mappedOutPort] = getOutputPort(this, queryOutput + ".#"); + // const [_, mappedOutPort] = getOutputPort(this, queryOutput + ".#"); - if (inPort && mappedOutPort) { - const lm = new DataMapperLinkModel(undefined, undefined, true, undefined, true); + // if (inPort && mappedOutPort) { + // const lm = new DataMapperLinkModel(undefined, undefined, true, undefined, true); - lm.setTargetPort(mappedOutPort); - lm.setSourcePort(inPort); - inPort.addLinkedPort(mappedOutPort); + // lm.setTargetPort(mappedOutPort); + // lm.setSourcePort(inPort); + // inPort.addLinkedPort(mappedOutPort); - lm.addLabel(new ExpressionLabelModel({ - isQuery: true - })); + // lm.addLabel(new ExpressionLabelModel({ + // isQuery: true + // })); - this.getModel().addAll(lm as any); - } + // this.getModel().addAll(lm as any); + // } } diff --git a/workspaces/ballerina/data-mapper/src/utils/model-utils.ts b/workspaces/ballerina/data-mapper/src/utils/model-utils.ts index e48f3a5c39e..cfd7951d5c2 100644 --- a/workspaces/ballerina/data-mapper/src/utils/model-utils.ts +++ b/workspaces/ballerina/data-mapper/src/utils/model-utils.ts @@ -16,7 +16,7 @@ * under the License. */ -import { ExpandedDMModel, IOType, Mapping } from "@wso2/ballerina-core"; +import { ExpandedDMModel, IOType, Mapping, Query } from "@wso2/ballerina-core"; import { BaseVisitor } from "../visitors/BaseVisitor"; export function traverseNode(model: ExpandedDMModel, visitor: BaseVisitor) { @@ -43,6 +43,11 @@ export function traverseNode(model: ExpandedDMModel, visitor: BaseVisitor) { } } + // Visit query + if (model.query) { + traverseQuery(model.query, model, visitor); + } + visitor.endVisit?.(model); } @@ -76,3 +81,8 @@ function traverseMappings(mappings: Mapping[], parentMapping: Mapping, parentMod visitor.endVisitMapping?.(mapping, parentMapping, parentModel); } } + +function traverseQuery(query: Query, parent: ExpandedDMModel, visitor: BaseVisitor) { + visitor.beginVisitQuery?.(query, parent); + visitor.endVisitQuery?.(query, parent); +} diff --git a/workspaces/ballerina/data-mapper/src/visitors/BaseVisitor.ts b/workspaces/ballerina/data-mapper/src/visitors/BaseVisitor.ts index d0f5d48b952..c0f6b9b2a2e 100644 --- a/workspaces/ballerina/data-mapper/src/visitors/BaseVisitor.ts +++ b/workspaces/ballerina/data-mapper/src/visitors/BaseVisitor.ts @@ -16,7 +16,7 @@ * under the License. */ -import { ExpandedDMModel, IOType, Mapping } from "@wso2/ballerina-core"; +import { ExpandedDMModel, IOType, Mapping, Query } from "@wso2/ballerina-core"; export interface BaseVisitor { beginVisit?(node: ExpandedDMModel, parent?: ExpandedDMModel): void; @@ -33,4 +33,8 @@ export interface BaseVisitor { beginVisitMapping?(node: Mapping, parentMapping: Mapping, parentModel?: ExpandedDMModel): void; endVisitMapping?(node: Mapping, parentMapping: Mapping, parentModel?: ExpandedDMModel): void; + + beginVisitQuery?(query: Query, parent?: ExpandedDMModel): void; + endVisitQuery?(query: Query, parent?: ExpandedDMModel): void; + } diff --git a/workspaces/ballerina/data-mapper/src/visitors/IntermediateNodeInitVisitor.ts b/workspaces/ballerina/data-mapper/src/visitors/IntermediateNodeInitVisitor.ts index f8cecbcd587..c757ee1245a 100644 --- a/workspaces/ballerina/data-mapper/src/visitors/IntermediateNodeInitVisitor.ts +++ b/workspaces/ballerina/data-mapper/src/visitors/IntermediateNodeInitVisitor.ts @@ -15,12 +15,11 @@ * specific language governing permissions and limitations * under the License. */ -import { LinkConnectorNode } from "../components/Diagram/Node"; +import { LinkConnectorNode, QueryExprConnectorNode, ClauseConnectorNode } from "../components/Diagram/Node"; import { DataMapperNodeModel } from "../components/Diagram/Node/commons/DataMapperNode"; import { DataMapperContext } from "../utils/DataMapperContext/DataMapperContext"; -import { Mapping } from "@wso2/ballerina-core"; +import { Mapping, Query } from "@wso2/ballerina-core"; import { BaseVisitor } from "./BaseVisitor"; -import { QueryExprConnectorNode } from "../components/Diagram/Node/QueryExprConnector"; export class IntermediateNodeInitVisitor implements BaseVisitor { private intermediateNodes: DataMapperNodeModel[] = []; @@ -45,6 +44,11 @@ export class IntermediateNodeInitVisitor implements BaseVisitor { } } + beginVisitQuery(query: Query): void { + const clauseConnectorNode = new ClauseConnectorNode(this.context, query); + this.intermediateNodes.push(clauseConnectorNode); + } + getNodes() { return this.intermediateNodes; } From 7d6492dea30647ccb251591d50009b6ca20766d9 Mon Sep 17 00:00:00 2001 From: ChamodA Date: Mon, 3 Nov 2025 11:27:36 +0530 Subject: [PATCH 04/20] Add ClauseConnectorNode support in IONodesScrollCanvasAction and positioning in Diagram --- .../components/Diagram/Actions/IONodesScrollCanvasAction.ts | 4 ++-- .../data-mapper/src/components/Diagram/Diagram.tsx | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/workspaces/ballerina/data-mapper/src/components/Diagram/Actions/IONodesScrollCanvasAction.ts b/workspaces/ballerina/data-mapper/src/components/Diagram/Actions/IONodesScrollCanvasAction.ts index 8688fee82ca..436a2c0fb2e 100644 --- a/workspaces/ballerina/data-mapper/src/components/Diagram/Actions/IONodesScrollCanvasAction.ts +++ b/workspaces/ballerina/data-mapper/src/components/Diagram/Actions/IONodesScrollCanvasAction.ts @@ -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; @@ -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); } } diff --git a/workspaces/ballerina/data-mapper/src/components/Diagram/Diagram.tsx b/workspaces/ballerina/data-mapper/src/components/Diagram/Diagram.tsx index 63d6cd763f6..b989f16cda2 100644 --- a/workspaces/ballerina/data-mapper/src/components/Diagram/Diagram.tsx +++ b/workspaces/ballerina/data-mapper/src/components/Diagram/Diagram.tsx @@ -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'; @@ -158,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); From 15f0c9ee76fc5c7e45d0ec7f193f2a484b77467d Mon Sep 17 00:00:00 2001 From: ChamodA Date: Mon, 3 Nov 2025 11:32:03 +0530 Subject: [PATCH 05/20] Add focus link support for ClauseConnector --- .../src/components/Diagram/Actions/utils.ts | 6 +++-- .../ClauseConnector/ClauseConnectorNode.ts | 22 +++++++++---------- .../ClauseConnectorNodeWidget.tsx | 8 +++---- .../Diagram/utils/focus-positioning-utils.ts | 4 ++-- 4 files changed, 21 insertions(+), 19 deletions(-) diff --git a/workspaces/ballerina/data-mapper/src/components/Diagram/Actions/utils.ts b/workspaces/ballerina/data-mapper/src/components/Diagram/Actions/utils.ts index aa47689df42..f148ecc2ada 100644 --- a/workspaces/ballerina/data-mapper/src/components/Diagram/Actions/utils.ts +++ b/workspaces/ballerina/data-mapper/src/components/Diagram/Actions/utils.ts @@ -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"; @@ -44,7 +45,8 @@ export const OUTPUT_NODES = [ export const INTERMEDIATE_NODES = [ LinkConnectorNode, - QueryExprConnectorNode + QueryExprConnectorNode, + ClauseConnectorNode ]; export const MIN_VISIBLE_HEIGHT = 68; diff --git a/workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNode.ts b/workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNode.ts index 25d26bdaf74..5775b234a8c 100644 --- a/workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNode.ts +++ b/workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNode.ts @@ -36,7 +36,7 @@ const NODE_ID = "clause-connector-node"; export class ClauseConnectorNode extends DataMapperNodeModel { public sourcePorts: InputOutputPortModel[] = []; - public targetPort: InputOutputPortModel; + public targetMappedPort: InputOutputPortModel; public inPort: IntermediatePortModel; public outPort: IntermediatePortModel; @@ -62,7 +62,7 @@ export class ClauseConnectorNode extends DataMapperNodeModel { initPorts(): void { const prevSourcePorts = this.sourcePorts; this.sourcePorts = []; - this.targetPort = undefined; + this.targetMappedPort = undefined; this.inPort = new IntermediatePortModel(`${this.query.inputs.join('_')}_${this.query.output}_IN`, "IN"); this.outPort = new IntermediatePortModel(`${this.query.inputs.join('_')}_${this.query.output}_OUT`, "OUT"); this.addPort(this.inPort); @@ -98,7 +98,7 @@ export class ClauseConnectorNode extends DataMapperNodeModel { if (node instanceof QueryOutputNode) { const targetPortPrefix = getTargetPortPrefix(node); - this.targetPort = node.getPort(`${targetPortPrefix}.${this.query.output}.#.IN`) as InputOutputPortModel; + this.targetMappedPort = node.getPort(`${targetPortPrefix}.${this.query.output}.#.IN`) as InputOutputPortModel; if (prevSourcePorts.length !== this.sourcePorts.length || prevSourcePorts.map(port => port.getID()).join('') !== this.sourcePorts.map(port => port.getID()).join('')) { @@ -120,7 +120,7 @@ export class ClauseConnectorNode extends DataMapperNodeModel { if (sourcePort) { sourcePort.addLinkedPort(this.inPort); - sourcePort.addLinkedPort(this.targetPort) + sourcePort.addLinkedPort(this.targetMappedPort) lm.setTargetPort(this.inPort); lm.setSourcePort(sourcePort); @@ -139,13 +139,13 @@ export class ClauseConnectorNode extends DataMapperNodeModel { } }) - if (this.targetPort) { + if (this.targetMappedPort) { const outPort = this.outPort; - const targetPort = this.targetPort; + const targetPort = this.targetMappedPort; const lm = new DataMapperLinkModel(undefined, this.diagnostics, true, undefined, true); - lm.setTargetPort(this.targetPort); + lm.setTargetPort(this.targetMappedPort); lm.setSourcePort(this.outPort); lm.registerListener({ selectionChanged(event) { @@ -160,8 +160,8 @@ export class ClauseConnectorNode extends DataMapperNodeModel { }) if (!this.label) { - const fieldFQN = this.targetPort.attributes.fieldFQN; - this.label = fieldFQN ? this.targetPort.attributes.fieldFQN.split('.').pop() : ''; + const fieldFQN = this.targetMappedPort.attributes.fieldFQN; + this.label = fieldFQN ? this.targetMappedPort.attributes.fieldFQN.split('.').pop() : ''; } this.getModel().addAll(lm as any); } @@ -170,8 +170,8 @@ export class ClauseConnectorNode extends DataMapperNodeModel { } public updatePosition() { - if (this.targetPort) { - const position = this.targetPort.getPosition(); + if (this.targetMappedPort) { + const position = this.targetMappedPort.getPosition(); this.setPosition( this.hasError() ? OFFSETS.LINK_CONNECTOR_NODE_WITH_ERROR.X diff --git a/workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNodeWidget.tsx b/workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNodeWidget.tsx index 2f97be030c9..ea0a2941e80 100644 --- a/workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNodeWidget.tsx +++ b/workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNodeWidget.tsx @@ -47,13 +47,13 @@ export function ClauseConnectorNodeWidget(props: ClauseConnectorNodeWidgetProps) const [deleteInProgress, setDeleteInProgress] = useState(false); const onClickEdit = () => { - const targetPort = node.targetPort; + const targetPort = node.targetMappedPort; setExprBarFocusedPort(targetPort); }; const onFocusClause = () => { const sourcePorts = node.sourcePorts.map(port => port.attributes.portName); - const targetPort = node.targetPort.attributes.portName; + const targetPort = node.targetMappedPort.attributes.portName; sourcePorts.forEach((port) => { collapsedFieldsStore.removeField(port); @@ -64,7 +64,7 @@ export function ClauseConnectorNodeWidget(props: ClauseConnectorNodeWidgetProps) const context = node.context; const lastView = context.views[context.views.length - 1]; - const mapping = node.targetPort.attributes.value; + const mapping = node.targetMappedPort.attributes.value; expandArrayFn(context, mapping.inputs[0], mapping.output, lastView.targetField); }; @@ -82,7 +82,7 @@ export function ClauseConnectorNodeWidget(props: ClauseConnectorNodeWidgetProps) appearance="icon" tooltip="Map clause elements" onClick={onFocusClause} - data-testid={`expand-clause-fn-${node?.targetPort?.attributes.fieldFQN}`} + data-testid={`expand-clause-fn-${node?.targetMappedPort?.attributes.fieldFQN}`} > diff --git a/workspaces/ballerina/data-mapper/src/components/Diagram/utils/focus-positioning-utils.ts b/workspaces/ballerina/data-mapper/src/components/Diagram/utils/focus-positioning-utils.ts index 514b133425f..912ccc625db 100644 --- a/workspaces/ballerina/data-mapper/src/components/Diagram/utils/focus-positioning-utils.ts +++ b/workspaces/ballerina/data-mapper/src/components/Diagram/utils/focus-positioning-utils.ts @@ -17,7 +17,7 @@ */ import { DiagramModel, NodeModel, PortModel } from "@projectstorm/react-diagrams"; -import { LinkConnectorNode, QueryExprConnectorNode } from "../Node"; +import { ClauseConnectorNode, LinkConnectorNode, QueryExprConnectorNode } from "../Node"; import { isInputNode, isIntermediateNode, isOutputNode } from "../Actions/utils"; import { animateNodesWithIndividualOffsets, @@ -157,7 +157,7 @@ export const resolveTargetNodeAndPort = ( let targetPort = targetNode?.getPort(targetPortId); if (isIntermediateNode(targetNode)) { - const intermediateNode = targetNode as LinkConnectorNode | QueryExprConnectorNode; + const intermediateNode = targetNode as LinkConnectorNode | QueryExprConnectorNode | ClauseConnectorNode; const intermediatePort = intermediateNode.targetMappedPort; targetNode = intermediatePort?.getNode(); From 5391f8597029a554f748d4a0dfffb874a941e37c Mon Sep 17 00:00:00 2001 From: ChamodA Date: Mon, 3 Nov 2025 14:03:16 +0530 Subject: [PATCH 06/20] Add JOIN clause to IntermediateClauseType and extend IntermediateClauseProps --- .../ballerina/ballerina-core/src/interfaces/data-mapper.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/workspaces/ballerina/ballerina-core/src/interfaces/data-mapper.ts b/workspaces/ballerina/ballerina-core/src/interfaces/data-mapper.ts index f8c7aca78c5..fedb6272733 100644 --- a/workspaces/ballerina/ballerina-core/src/interfaces/data-mapper.ts +++ b/workspaces/ballerina/ballerina-core/src/interfaces/data-mapper.ts @@ -58,7 +58,8 @@ export enum IntermediateClauseType { WHERE = "where", FROM = "from", ORDER_BY = "order by", - LIMIT = "limit" + LIMIT = "limit", + JOIN = "join", } export enum ResultClauseType { @@ -205,6 +206,9 @@ export interface IntermediateClauseProps { type?: string; expression: string; order?: "ascending" | "descending"; + lhsExpression?: string; + rhsExpression?: string; + isOuter?: boolean; } export interface IntermediateClause { From 1c778674c8ba34e868132a238e3b0ded5bde35f0 Mon Sep 17 00:00:00 2001 From: ChamodA Date: Mon, 3 Nov 2025 14:11:28 +0530 Subject: [PATCH 07/20] Remove focusedMemberId handling from IOType and refactor InputNode and DataMapperNodeModel --- .../ballerina-core/src/interfaces/data-mapper.ts | 1 - .../src/components/Diagram/Node/Input/InputNode.ts | 7 ------- .../Diagram/Node/commons/DataMapperNode.ts | 13 +------------ 3 files changed, 1 insertion(+), 20 deletions(-) diff --git a/workspaces/ballerina/ballerina-core/src/interfaces/data-mapper.ts b/workspaces/ballerina/ballerina-core/src/interfaces/data-mapper.ts index fedb6272733..5e8c0ce45e9 100644 --- a/workspaces/ballerina/ballerina-core/src/interfaces/data-mapper.ts +++ b/workspaces/ballerina/ballerina-core/src/interfaces/data-mapper.ts @@ -94,7 +94,6 @@ export interface IOType { members?: IOType[]; defaultValue?: unknown; optional?: boolean; - focusedMemberId?: string; isFocused?: boolean; isRecursive?: boolean; isDeepNested?: boolean; diff --git a/workspaces/ballerina/data-mapper/src/components/Diagram/Node/Input/InputNode.ts b/workspaces/ballerina/data-mapper/src/components/Diagram/Node/Input/InputNode.ts index e3337a1776d..456c46100fe 100644 --- a/workspaces/ballerina/data-mapper/src/components/Diagram/Node/Input/InputNode.ts +++ b/workspaces/ballerina/data-mapper/src/components/Diagram/Node/Input/InputNode.ts @@ -100,13 +100,6 @@ export class InputNode extends DataMapperNodeModel { }); } } else if (this.filteredInputType.kind === TypeKind.Array) { - const focusedMemberId = this.filteredInputType?.focusedMemberId; - if (focusedMemberId) { - const focusedMemberField = this.context.model.inputs.find(input => input.id === focusedMemberId); - if (focusedMemberField) { - this.filteredInputType.member = focusedMemberField; - } - } this.numberOfFields += await this.addPortsForInputField({ field: this.filteredInputType?.member, portType: "OUT", diff --git a/workspaces/ballerina/data-mapper/src/components/Diagram/Node/commons/DataMapperNode.ts b/workspaces/ballerina/data-mapper/src/components/Diagram/Node/commons/DataMapperNode.ts index 4912f5006b3..cd32e27a636 100644 --- a/workspaces/ballerina/data-mapper/src/components/Diagram/Node/commons/DataMapperNode.ts +++ b/workspaces/ballerina/data-mapper/src/components/Diagram/Node/commons/DataMapperNode.ts @@ -381,7 +381,7 @@ export abstract class DataMapperNodeModel extends NodeModel { - const memberField = this.resolveArrayMemberField(attributes); + const memberField = attributes.field?.member; return await this.addPortsForInputField({ ...attributes, hidden: isHidden, @@ -390,17 +390,6 @@ export abstract class DataMapperNodeModel extends NodeModel input.id === focusedMemberId); - if (focusedMemberField) { - return focusedMemberField; - } - } - return attributes.field?.member; - } - private async processRecordField(attributes: OutputPortAttributes) { const fields = attributes.field?.fields?.filter(f => !!f); if (fields && fields.length) { From 6ae1e871402e39941be7ec145223085cdba4a757 Mon Sep 17 00:00:00 2001 From: ChamodA Date: Mon, 3 Nov 2025 14:24:51 +0530 Subject: [PATCH 08/20] Refactor ExpressionLabelFactory and ExpressionLabelModel to remove QueryExprLabelWidget references and clean up QueryOutputNode by removing unused query handling code --- .../Diagram/Label/ExpressionLabelFactory.tsx | 4 - .../Diagram/Label/ExpressionLabelModel.ts | 3 - .../Diagram/Label/QueryExprLabelWidget.tsx | 123 ------------------ .../Node/QueryOutput/QueryOutputNode.ts | 25 ---- 4 files changed, 155 deletions(-) delete mode 100644 workspaces/ballerina/data-mapper/src/components/Diagram/Label/QueryExprLabelWidget.tsx diff --git a/workspaces/ballerina/data-mapper/src/components/Diagram/Label/ExpressionLabelFactory.tsx b/workspaces/ballerina/data-mapper/src/components/Diagram/Label/ExpressionLabelFactory.tsx index 7e25a57183b..5dbf3b6a993 100644 --- a/workspaces/ballerina/data-mapper/src/components/Diagram/Label/ExpressionLabelFactory.tsx +++ b/workspaces/ballerina/data-mapper/src/components/Diagram/Label/ExpressionLabelFactory.tsx @@ -22,7 +22,6 @@ import { DiagramEngine } from '@projectstorm/react-diagrams'; import { ExpressionLabelModel } from './ExpressionLabelModel'; import { ExpressionLabelWidget } from './ExpressionLabelWidget'; -import { QueryExprLabelWidget } from './QueryExprLabelWidget'; import { MappingOptionsWidget } from './MappingOptionsWidget'; export class ExpressionLabelFactory extends AbstractReactFactory { @@ -35,9 +34,6 @@ export class ExpressionLabelFactory extends AbstractReactFactory): JSX.Element { - if (event.model.isQuery) { - return ; - } if (event.model.link?.pendingMappingType) { return ; } diff --git a/workspaces/ballerina/data-mapper/src/components/Diagram/Label/ExpressionLabelModel.ts b/workspaces/ballerina/data-mapper/src/components/Diagram/Label/ExpressionLabelModel.ts index 5cef5a7b0df..8808cc1dea8 100644 --- a/workspaces/ballerina/data-mapper/src/components/Diagram/Label/ExpressionLabelModel.ts +++ b/workspaces/ballerina/data-mapper/src/components/Diagram/Label/ExpressionLabelModel.ts @@ -28,7 +28,6 @@ export interface ExpressionLabelOptions extends BaseModelOptions { link?: DataMapperLinkModel; field?: Node; editorLabel?: string; - isQuery?: boolean; collectClauseFn?: string; deleteLink?: () => void; } @@ -37,7 +36,6 @@ export class ExpressionLabelModel extends LabelModel { context?: IDataMapperContext; link?: DataMapperLinkModel; value?: string; - isQuery?: boolean; collectClauseFn?: string; deleteLink?: () => void; @@ -49,7 +47,6 @@ export class ExpressionLabelModel extends LabelModel { this.context = options.context; this.link = options.link; this.value = options.value || ''; - this.isQuery = options.isQuery; this.collectClauseFn = options.collectClauseFn; this.updateSource = this.updateSource.bind(this); this.deleteLink = options.deleteLink; diff --git a/workspaces/ballerina/data-mapper/src/components/Diagram/Label/QueryExprLabelWidget.tsx b/workspaces/ballerina/data-mapper/src/components/Diagram/Label/QueryExprLabelWidget.tsx deleted file mode 100644 index 53a4489b471..00000000000 --- a/workspaces/ballerina/data-mapper/src/components/Diagram/Label/QueryExprLabelWidget.tsx +++ /dev/null @@ -1,123 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -// tslint:disable: jsx-no-multiline-js -import React, { MouseEvent, ReactNode } from 'react'; - -import { Button, Codicon } from '@wso2/ui-toolkit'; -import { css } from '@emotion/css'; -import classNames from "classnames"; - -import { ExpressionLabelModel } from './ExpressionLabelModel'; -import { useDMQueryClausesPanelStore } from '../../../store/store'; - - -export interface QueryExprLabelWidgetProps { - model: ExpressionLabelModel; -} - -export const useStyles = () => ({ - container: css({ - width: '100%', - backgroundColor: "var(--vscode-sideBar-background)", - padding: "2px", - borderRadius: "6px", - display: "flex", - color: "var(--vscode-checkbox-border)", - alignItems: "center", - "& > vscode-button > *": { - margin: "0 2px" - } - }), - btnContainer: css({ - display: "flex", - flexDirection: "row", - justifyContent: "space-between", - alignItems: "center", - "& > *": { - margin: "0 2px" - } - }), - deleteIconButton: css({ - color: 'var(--vscode-checkbox-border)', - }), - separator: css({ - height: 'fit-content', - width: '1px', - backgroundColor: 'var(--vscode-editor-lineHighlightBorder)', - }), - rightBorder: css({ - borderRightWidth: '2px', - borderColor: 'var(--vscode-pickerGroup-border)', - }), - loadingContainer: css({ - padding: '10px', - }), -}); - -export function QueryExprLabelWidget(props: QueryExprLabelWidgetProps) { - - const classes = useStyles(); - const { link, value} = props.model; - const diagnostic = link && link.hasError() ? link.diagnostics[0] || link.diagnostics[0] : null; - - const { setIsQueryClausesPanelOpen } = useDMQueryClausesPanelStore(); - const onClickOpenClausePanel = (evt?: MouseEvent) => { - setIsQueryClausesPanelOpen(true); - }; - - const elements: ReactNode[] = [ - ( -
- -
- ), - ]; - - return ( -
-
- -
-
- ); -} diff --git a/workspaces/ballerina/data-mapper/src/components/Diagram/Node/QueryOutput/QueryOutputNode.ts b/workspaces/ballerina/data-mapper/src/components/Diagram/Node/QueryOutput/QueryOutputNode.ts index a3203d16f8e..9260b81d0f5 100644 --- a/workspaces/ballerina/data-mapper/src/components/Diagram/Node/QueryOutput/QueryOutputNode.ts +++ b/workspaces/ballerina/data-mapper/src/components/Diagram/Node/QueryOutput/QueryOutputNode.ts @@ -115,7 +115,6 @@ export class QueryOutputNode extends DataMapperNodeModel { const views = this.context.views; const lastViewIndex = views.length - 1; - // const { inputs: queryInputs, output: queryOutput} = this.context.model.query; mappings.forEach((mapping) => { @@ -163,30 +162,6 @@ export class QueryOutputNode extends DataMapperNodeModel { this.getModel().addAll(lm as any); } }); - - // const inputNode = findInputNode(queryInputs[0], this, views, lastViewIndex); - // let inPort: InputOutputPortModel; - // if (inputNode) { - // inPort = getInputPort(inputNode, queryInputs[0].replace(/\.\d+/g, '')); - // } - - // const [_, mappedOutPort] = getOutputPort(this, queryOutput + ".#"); - - // if (inPort && mappedOutPort) { - // const lm = new DataMapperLinkModel(undefined, undefined, true, undefined, true); - - // lm.setTargetPort(mappedOutPort); - // lm.setSourcePort(inPort); - // inPort.addLinkedPort(mappedOutPort); - - // lm.addLabel(new ExpressionLabelModel({ - // isQuery: true - // })); - - // this.getModel().addAll(lm as any); - // } - - } async deleteField(mapping: Mapping) { From 1e00af88cd6ae210b18b70f2cca00894beb934b7 Mon Sep 17 00:00:00 2001 From: ChamodA Date: Tue, 4 Nov 2025 16:14:13 +0530 Subject: [PATCH 09/20] Support focused view with multiple source fields --- .../src/interfaces/data-mapper.ts | 3 +++ .../src/rpc-managers/data-mapper/utils.ts | 14 ++++++++++++-- .../components/DataMapper/DataMapperEditor.tsx | 2 +- .../DataMapper/Views/DataMapperView.ts | 2 +- .../components/Diagram/Node/Input/InputNode.ts | 2 +- .../QueryExprConnectorNodeWidget.tsx | 2 +- .../components/Diagram/utils/common-utils.ts | 5 ++--- .../Diagram/utils/modification-utils.ts | 2 +- .../src/components/Diagram/utils/node-utils.ts | 17 ++++++++--------- .../src/components/Diagram/utils/port-utils.ts | 2 +- 10 files changed, 31 insertions(+), 20 deletions(-) diff --git a/workspaces/ballerina/ballerina-core/src/interfaces/data-mapper.ts b/workspaces/ballerina/ballerina-core/src/interfaces/data-mapper.ts index 5e8c0ce45e9..30f0c1a16c7 100644 --- a/workspaces/ballerina/ballerina-core/src/interfaces/data-mapper.ts +++ b/workspaces/ballerina/ballerina-core/src/interfaces/data-mapper.ts @@ -125,6 +125,7 @@ export interface ExpandedDMModel { query?: Query; mapping_fields?: Record; triggerRefresh?: boolean; + focusInputRootMap?: Record; } export interface DMModel { @@ -138,6 +139,8 @@ export interface DMModel { focusInputs?: Record; mapping_fields?: Record; triggerRefresh?: boolean; + traversingRoot?: string; + focusInputRootMap?: Record; } export interface ModelState { diff --git a/workspaces/ballerina/ballerina-extension/src/rpc-managers/data-mapper/utils.ts b/workspaces/ballerina/ballerina-extension/src/rpc-managers/data-mapper/utils.ts index e913aecb276..947cc30e4f8 100644 --- a/workspaces/ballerina/ballerina-extension/src/rpc-managers/data-mapper/utils.ts +++ b/workspaces/ballerina/ballerina-extension/src/rpc-managers/data-mapper/utils.ts @@ -460,7 +460,8 @@ export function expandDMModel( query: model.query, source: "", rootViewId, - triggerRefresh: model.triggerRefresh + triggerRefresh: model.triggerRefresh, + focusInputRootMap: model.focusInputRootMap }; } @@ -478,13 +479,18 @@ function processInputRoots(model: DMModel): IOType[] { inputs.push(input); } } + + model.focusInputRootMap = {}; 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); + }); } /** @@ -592,6 +598,10 @@ function processArray( parentId = member.name; fieldId = member.name; isFocused = true; + + if (model.traversingRoot){ + model.focusInputRootMap[parentId] = model.traversingRoot; + } } } diff --git a/workspaces/ballerina/data-mapper/src/components/DataMapper/DataMapperEditor.tsx b/workspaces/ballerina/data-mapper/src/components/DataMapper/DataMapperEditor.tsx index 399ecd16e14..2766df14269 100644 --- a/workspaces/ballerina/data-mapper/src/components/DataMapper/DataMapperEditor.tsx +++ b/workspaces/ballerina/data-mapper/src/components/DataMapper/DataMapperEditor.tsx @@ -150,7 +150,7 @@ export function DataMapperEditor(props: DataMapperEditorProps) { hasInputsOutputsChanged = false } = modelState; - const initialView = [{ + const initialView: View[] = [{ label: model.output.name, targetField: name }]; diff --git a/workspaces/ballerina/data-mapper/src/components/DataMapper/Views/DataMapperView.ts b/workspaces/ballerina/data-mapper/src/components/DataMapper/Views/DataMapperView.ts index 8a830681ad4..4ad0eadb597 100644 --- a/workspaces/ballerina/data-mapper/src/components/DataMapper/Views/DataMapperView.ts +++ b/workspaces/ballerina/data-mapper/src/components/DataMapper/Views/DataMapperView.ts @@ -17,7 +17,7 @@ */ export interface View { label: string; - sourceField?: string; + sourceFields?: string[]; targetField: string; subMappingInfo?: SubMappingInfo; } diff --git a/workspaces/ballerina/data-mapper/src/components/Diagram/Node/Input/InputNode.ts b/workspaces/ballerina/data-mapper/src/components/Diagram/Node/Input/InputNode.ts index 456c46100fe..a6c6df45c90 100644 --- a/workspaces/ballerina/data-mapper/src/components/Diagram/Node/Input/InputNode.ts +++ b/workspaces/ballerina/data-mapper/src/components/Diagram/Node/Input/InputNode.ts @@ -55,7 +55,7 @@ export class InputNode extends DataMapperNodeModel { const collapsedFields = useDMCollapsedFieldsStore.getState().fields; const expandedFields = useDMExpandedFieldsStore.getState().fields; const focusedFieldFQNs = [ - ...this.context.views.map(view => view.sourceField).filter(Boolean), + ...this.context.views.flatMap(view => view.sourceFields).filter(Boolean), ...(this.context.model.query?.inputs || []) ]; const parentPort = this.addPortsForHeader({ diff --git a/workspaces/ballerina/data-mapper/src/components/Diagram/Node/QueryExprConnector/QueryExprConnectorNodeWidget.tsx b/workspaces/ballerina/data-mapper/src/components/Diagram/Node/QueryExprConnector/QueryExprConnectorNodeWidget.tsx index 51f2c91f80f..28c799206a6 100644 --- a/workspaces/ballerina/data-mapper/src/components/Diagram/Node/QueryExprConnector/QueryExprConnectorNodeWidget.tsx +++ b/workspaces/ballerina/data-mapper/src/components/Diagram/Node/QueryExprConnector/QueryExprConnectorNodeWidget.tsx @@ -73,7 +73,7 @@ export function QueryExprConnectorNodeWidget(props: QueryExprConnectorNodeWidget const context = node.context; const lastView = context.views[context.views.length - 1]; const mapping = node.targetMappedPort.attributes.value; - expandArrayFn(context, mapping.inputs[0], mapping.output, lastView.targetField); + expandArrayFn(context, mapping.inputs, mapping.output, lastView.targetField); }; const loadingScreen = ( diff --git a/workspaces/ballerina/data-mapper/src/components/Diagram/utils/common-utils.ts b/workspaces/ballerina/data-mapper/src/components/Diagram/utils/common-utils.ts index 1871503886c..efc6323d196 100644 --- a/workspaces/ballerina/data-mapper/src/components/Diagram/utils/common-utils.ts +++ b/workspaces/ballerina/data-mapper/src/components/Diagram/utils/common-utils.ts @@ -31,7 +31,6 @@ import { InputNode, OBJECT_OUTPUT_NODE_TYPE, PRIMITIVE_OUTPUT_NODE_TYPE, - PrimitiveOutputNode, QueryOutputNode } from "../Node"; import { IDataMapperContext } from "../../../utils/DataMapperContext/DataMapperContext"; @@ -218,7 +217,7 @@ export function getErrorKind(node: DataMapperNodeModel): ErrorNodeKind { } } -export function expandArrayFn(context: IDataMapperContext, inputId: string, outputId: string, viewId: string): void { +export function expandArrayFn(context: IDataMapperContext, inputIds: string[], outputId: string, viewId: string): void { const { addView, views } = context; @@ -242,7 +241,7 @@ export function expandArrayFn(context: IDataMapperContext, inputId: string, outp // Create base view properties const baseView: View = { label: label, - sourceField: inputId, + sourceFields: inputIds, targetField: targetField }; diff --git a/workspaces/ballerina/data-mapper/src/components/Diagram/utils/modification-utils.ts b/workspaces/ballerina/data-mapper/src/components/Diagram/utils/modification-utils.ts index 4a203ac8d58..6fdd51db0d3 100644 --- a/workspaces/ballerina/data-mapper/src/components/Diagram/utils/modification-utils.ts +++ b/workspaces/ballerina/data-mapper/src/components/Diagram/utils/modification-utils.ts @@ -172,7 +172,7 @@ export async function mapWithQuery(link: DataMapperLinkModel, clauseType: Result await context.convertToQuery(mapping, clauseType, viewId, name); - expandArrayFn(context, input, output, viewId); + expandArrayFn(context, [input], output, viewId); } export function buildInputAccessExpr(fieldFqn: string): string { diff --git a/workspaces/ballerina/data-mapper/src/components/Diagram/utils/node-utils.ts b/workspaces/ballerina/data-mapper/src/components/Diagram/utils/node-utils.ts index dfda290fa33..9e200e5e290 100644 --- a/workspaces/ballerina/data-mapper/src/components/Diagram/utils/node-utils.ts +++ b/workspaces/ballerina/data-mapper/src/components/Diagram/utils/node-utils.ts @@ -20,28 +20,27 @@ import { InputNode, SubMappingNode } from "../Node"; import { View } from "../../DataMapper/Views/DataMapperView"; -export function findInputNode(field: string, outputNode: DataMapperNodeModel, views?: View[], lastViewIndex?: number): InputNode { +export function findInputNode(field: string, outputNode: DataMapperNodeModel, views?: View[], lastViewIndex?: number): InputNode | SubMappingNode | undefined { const nodes = outputNode.getModel().getNodes(); // Helper function to find input node by field path - const findNodeByField = (fieldPath: string): InputNode | undefined => { - const mappingStartsWith = fieldPath.split('.')[0]; + const findNodeByField = (fieldStartsWith: string): InputNode | SubMappingNode | undefined => { return nodes.find(node => { if (node instanceof InputNode) { - return node.inputType.id === mappingStartsWith; + return node.inputType.id === fieldStartsWith; } else if (node instanceof SubMappingNode) { - return node.subMappings.some(subMapping => subMapping.name === mappingStartsWith); + return node.subMappings.some(subMapping => subMapping.name === fieldStartsWith); } - }) as InputNode | undefined; + }) as InputNode | SubMappingNode | undefined; }; // try finding input node using 'field' (map from other input ports) - let inputNode = findNodeByField(field); + const fieldStartsWith = field.split('.')[0]; + let inputNode = findNodeByField(fieldStartsWith); // if not found, try with parentSourceField if (!inputNode && views && lastViewIndex) { - const parentSourceField = views[lastViewIndex].sourceField; - inputNode = findInputNode(parentSourceField, outputNode, views, lastViewIndex - 1); + inputNode = findNodeByField(outputNode.context.model.focusInputRootMap[fieldStartsWith]); } return inputNode; diff --git a/workspaces/ballerina/data-mapper/src/components/Diagram/utils/port-utils.ts b/workspaces/ballerina/data-mapper/src/components/Diagram/utils/port-utils.ts index 87022cdaf13..a4f34f5b280 100644 --- a/workspaces/ballerina/data-mapper/src/components/Diagram/utils/port-utils.ts +++ b/workspaces/ballerina/data-mapper/src/components/Diagram/utils/port-utils.ts @@ -23,7 +23,7 @@ import { ARRAY_OUTPUT_TARGET_PORT_PREFIX, OBJECT_OUTPUT_TARGET_PORT_PREFIX, PRIM import { ArrayOutputNode } from "../Node/ArrayOutput/ArrayOutputNode"; import { PrimitiveOutputNode } from "../Node/PrimitiveOutput/PrimitiveOutputNode"; -export function getInputPort(node: InputNode, inputField: string): InputOutputPortModel { +export function getInputPort(node: InputNode | SubMappingNode, inputField: string): InputOutputPortModel { const portId = node instanceof SubMappingNode ? `${SUB_MAPPING_INPUT_SOURCE_PORT_PREFIX}.${inputField}.OUT` : `${inputField}.OUT`; From 91a02ba74afae357cca19be86e1082fa862078c6 Mon Sep 17 00:00:00 2001 From: ChamodA Date: Tue, 4 Nov 2025 20:56:29 +0530 Subject: [PATCH 10/20] Enhance clause support by adding JOIN clause support and updating model signature to include query inputs --- .../src/views/DataMapper/DataMapperView.tsx | 2 +- .../SidePanel/QueryClauses/ClauseEditor.tsx | 41 ++++++++++++++++++- .../ClauseConnectorNodeWidget.tsx | 33 ++++----------- .../Diagram/hooks/useDiagramModel.ts | 4 +- 4 files changed, 52 insertions(+), 28 deletions(-) diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/DataMapper/DataMapperView.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/DataMapper/DataMapperView.tsx index 916d8e0d90d..39f51e37187 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/DataMapper/DataMapperView.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/DataMapper/DataMapperView.tsx @@ -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) : '' diff --git a/workspaces/ballerina/data-mapper/src/components/DataMapper/SidePanel/QueryClauses/ClauseEditor.tsx b/workspaces/ballerina/data-mapper/src/components/DataMapper/SidePanel/QueryClauses/ClauseEditor.tsx index 13c4af1caf8..df5cf559dd0 100644 --- a/workspaces/ballerina/data-mapper/src/components/DataMapper/SidePanel/QueryClauses/ClauseEditor.tsx +++ b/workspaces/ballerina/data-mapper/src/components/DataMapper/SidePanel/QueryClauses/ClauseEditor.tsx @@ -40,7 +40,8 @@ export function ClauseEditor(props: ClauseEditorProps) { { 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 = { @@ -92,6 +93,42 @@ export function ClauseEditor(props: ClauseEditorProps) { items: ["ascending", "descending"] } + const isOuterField: DMFormField = { + key: "isOuter", + label: "Is Outer Join", + type: "FLAG", + optional: false, + editable: true, + documentation: "Specify whether the join is an outer join", + value: clauseProps?.isOuter ?? false, + valueTypeConstraint: "Global", + enabled: true, + } + + 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({ type: clauseType as IntermediateClauseType, @@ -107,6 +144,8 @@ export function ClauseEditor(props: ClauseEditorProps) { return [nameField, typeField, expressionField]; case IntermediateClauseType.ORDER_BY: return [expressionField, orderField]; + case IntermediateClauseType.JOIN: + return [nameField, typeField, expressionField, isOuterField, lhsExpressionField, rhsExpressionField]; default: return [expressionField]; } diff --git a/workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNodeWidget.tsx b/workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNodeWidget.tsx index ea0a2941e80..c9f59934b63 100644 --- a/workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNodeWidget.tsx +++ b/workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNodeWidget.tsx @@ -16,17 +16,16 @@ * under the License. */ // tslint:disable: jsx-no-multiline-js -import React, { useState } from "react"; +import React from "react"; import { DiagramEngine } from '@projectstorm/react-diagrams-core'; import { Button, Codicon, ProgressRing } from '@wso2/ui-toolkit'; import classnames from 'classnames'; import { useIntermediateNodeStyles } from '../../../styles'; import { ClauseConnectorNode } from './ClauseConnectorNode'; -import { renderDeleteButton, renderEditButton, renderPortWidget } from "../LinkConnector/LinkConnectorWidgetComponents"; +import { renderPortWidget } from "../LinkConnector/LinkConnectorWidgetComponents"; import { DiagnosticWidget } from "../../Diagnostic/DiagnosticWidget"; -import { expandArrayFn } from "../../utils/common-utils"; -import { useDMCollapsedFieldsStore, useDMExpandedFieldsStore, useDMExpressionBarStore } from "../../../../store/store"; +import { useDMExpressionBarStore, useDMQueryClausesPanelStore } from "../../../../store/store"; export interface ClauseConnectorNodeWidgetProps { node: ClauseConnectorNode; @@ -38,36 +37,20 @@ export function ClauseConnectorNodeWidget(props: ClauseConnectorNodeWidgetProps) const classes = useIntermediateNodeStyles(); const setExprBarFocusedPort = useDMExpressionBarStore(state => state.setFocusedPort); - const collapsedFieldsStore = useDMCollapsedFieldsStore(); - const expandedFieldsStore = useDMExpandedFieldsStore(); const diagnostic = node.hasError() ? node.diagnostics[0] : null; const value = node.value; - const [deleteInProgress, setDeleteInProgress] = useState(false); + const setIsQueryClausesPanelOpen = useDMQueryClausesPanelStore(state => state.setIsQueryClausesPanelOpen); + const onClickOpenClausePanel = () => { + setIsQueryClausesPanelOpen(true); + }; const onClickEdit = () => { const targetPort = node.targetMappedPort; setExprBarFocusedPort(targetPort); }; - const onFocusClause = () => { - const sourcePorts = node.sourcePorts.map(port => port.attributes.portName); - const targetPort = node.targetMappedPort.attributes.portName; - - sourcePorts.forEach((port) => { - collapsedFieldsStore.removeField(port); - expandedFieldsStore.removeField(port); - }); - collapsedFieldsStore.removeField(targetPort); - expandedFieldsStore.removeField(targetPort); - - const context = node.context; - const lastView = context.views[context.views.length - 1]; - const mapping = node.targetMappedPort.attributes.value; - expandArrayFn(context, mapping.inputs[0], mapping.output, lastView.targetField); - }; - const loadingScreen = (
@@ -81,7 +64,7 @@ export function ClauseConnectorNodeWidget(props: ClauseConnectorNodeWidgetProps)