diff --git a/workspaces/ballerina/ballerina-core/src/interfaces/data-mapper.ts b/workspaces/ballerina/ballerina-core/src/interfaces/data-mapper.ts index f8c7aca78c5..30f0c1a16c7 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 { @@ -93,7 +94,6 @@ export interface IOType { members?: IOType[]; defaultValue?: unknown; optional?: boolean; - focusedMemberId?: string; isFocused?: boolean; isRecursive?: boolean; isDeepNested?: boolean; @@ -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 { @@ -205,6 +208,9 @@ export interface IntermediateClauseProps { type?: string; expression: string; order?: "ascending" | "descending"; + lhsExpression?: string; + rhsExpression?: string; + isOuter?: boolean; } export interface IntermediateClause { 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 b0eb34f4b08..36c87a3a3e1 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 @@ -467,7 +467,8 @@ export function expandDMModel( query: model.query, source: "", rootViewId, - triggerRefresh: model.triggerRefresh + triggerRefresh: model.triggerRefresh, + focusInputRootMap: model.focusInputRootMap }; } @@ -485,13 +486,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); + }); } /** @@ -599,6 +605,10 @@ function processArray( parentId = member.name; fieldId = member.name; isFocused = true; + + if (model.traversingRoot){ + model.focusInputRootMap[parentId] = model.traversingRoot; + } } } diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/DataMapper/DataMapperView.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/DataMapper/DataMapperView.tsx index 91570511a88..9456770b65d 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/DataMapperEditor.tsx b/workspaces/ballerina/data-mapper/src/components/DataMapper/DataMapperEditor.tsx index 399ecd16e14..d0a73bd940e 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 }]; @@ -228,10 +228,10 @@ export function DataMapperEditor(props: DataMapperEditorProps) { const context = new DataMapperContext( model, views, + hasInputsOutputsChanged, addView, applyModifications, addArrayElement, - hasInputsOutputsChanged, convertToQuery, deleteMapping, deleteSubMapping, 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..75cb3a1ba3a 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 @@ -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; @@ -32,7 +33,8 @@ 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(); + const { type: _clauseType, properties: clauseProps } = clause ?? clauseToAdd ?? {}; const [clauseType, setClauseType] = React.useState(_clauseType ?? IntermediateClauseType.WHERE); const clauseTypeItems: OptionProps[] = [ @@ -40,16 +42,17 @@ 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 = { 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 a name for the variable", value: clauseProps?.name ?? "", valueTypeConstraint: "Global", enabled: true, @@ -61,7 +64,7 @@ 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, @@ -69,11 +72,11 @@ export function ClauseEditor(props: ClauseEditorProps) { 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, @@ -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 @@ -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]; } @@ -119,7 +159,7 @@ export function ClauseEditor(props: ClauseEditorProps) { cancelText: "Cancel", nestedForm: true, onSubmit: handleSubmit, - onCancel, + onCancel: handleCancel, isSaving } diff --git a/workspaces/ballerina/data-mapper/src/components/DataMapper/SidePanel/QueryClauses/ClausesPanel.tsx b/workspaces/ballerina/data-mapper/src/components/DataMapper/SidePanel/QueryClauses/ClausesPanel.tsx index 903f6870136..46042de03a0 100644 --- a/workspaces/ballerina/data-mapper/src/components/DataMapper/SidePanel/QueryClauses/ClausesPanel.tsx +++ b/workspaces/ballerina/data-mapper/src/components/DataMapper/SidePanel/QueryClauses/ClausesPanel.tsx @@ -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"; @@ -35,6 +35,7 @@ export interface ClausesPanelProps { export function ClausesPanel(props: ClausesPanelProps) { const { isQueryClausesPanelOpen, setIsQueryClausesPanelOpen } = useDMQueryClausesPanelStore(); + const { clauseToAdd, setClauseToAdd } = useDMQueryClausesPanelStore.getState(); const { query, targetField, addClauses, deleteClause, generateForm } = props; const [adding, setAdding] = React.useState(); @@ -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 ( - 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); 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..effc2dc4790 100644 --- a/workspaces/ballerina/data-mapper/src/components/Diagram/Label/ExpressionLabelFactory.tsx +++ b/workspaces/ballerina/data-mapper/src/components/Diagram/Label/ExpressionLabelFactory.tsx @@ -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 { constructor() { @@ -35,10 +35,10 @@ export class ExpressionLabelFactory extends AbstractReactFactory): JSX.Element { - if (event.model.isQuery) { - return ; - } if (event.model.link?.pendingMappingType) { + if(event.model.link.pendingMappingType === MappingType.ArrayJoin) { + return <>; + } return ; } 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/Link/DataMapperLink/DataMapperLink.ts b/workspaces/ballerina/data-mapper/src/components/Diagram/Link/DataMapperLink/DataMapperLink.ts index 8164ebafe6a..1a2be0c0dce 100644 --- a/workspaces/ballerina/data-mapper/src/components/Diagram/Link/DataMapperLink/DataMapperLink.ts +++ b/workspaces/ballerina/data-mapper/src/components/Diagram/Link/DataMapperLink/DataMapperLink.ts @@ -28,6 +28,7 @@ export enum MappingType { ArrayToArray = "array-array", ArrayToSingleton = "array-singleton", ArrayToSingletonAggregate = "array-singleton-aggregate", + ArrayJoin = "array-join", Incompatible = "incompatible", ContainsUnions = "contains-unions", Default = undefined // This is for non-array mappings currently diff --git a/workspaces/ballerina/data-mapper/src/components/Diagram/LinkState/CreateLinkState.ts b/workspaces/ballerina/data-mapper/src/components/Diagram/LinkState/CreateLinkState.ts index 23ee49d3f59..70f8eff1d75 100644 --- a/workspaces/ballerina/data-mapper/src/components/Diagram/LinkState/CreateLinkState.ts +++ b/workspaces/ballerina/data-mapper/src/components/Diagram/LinkState/CreateLinkState.ts @@ -29,8 +29,8 @@ import { getMappingType, handleExpand, isExpandable, isPendingMappingRequired } import { removePendingMappingTempLinkIfExists } from '../utils/link-utils'; import { useDMExpressionBarStore } from '../../../store/store'; import { IntermediatePortModel } from '../Port/IntermediatePort'; -import { LinkConnectorNode } from '../Node/LinkConnector/LinkConnectorNode'; import { isQueryHeaderPort } from '../utils/port-utils'; +import { ClauseConnectorNode, LinkConnectorNode } from '../Node'; /** * This state is controlling the creation of a link. */ @@ -97,13 +97,13 @@ export class CreateLinkState extends State { // select the target port of the link to create a mapping const targetPort = (element as DataMapperLinkModel).getTargetPort(); - if (targetPort instanceof InputOutputPortModel && !isQueryHeaderPort(targetPort)) { + if (targetPort instanceof InputOutputPortModel) { element = targetPort; } if (targetPort instanceof IntermediatePortModel) { const parentNode = targetPort.getNode(); - if (parentNode instanceof LinkConnectorNode) { + if (parentNode instanceof LinkConnectorNode || parentNode instanceof ClauseConnectorNode) { element = parentNode.targetMappedPort; } } @@ -120,7 +120,7 @@ export class CreateLinkState extends State { this.clearState(); this.eject(); } else if (element instanceof PortModel && !this.sourcePort) { - if (element instanceof InputOutputPortModel && !isQueryHeaderPort(element)) { + if (element instanceof InputOutputPortModel) { if (element.attributes.portType === "OUT") { this.sourcePort = element; element.fireEvent({}, "mappingStartedFrom"); @@ -142,7 +142,7 @@ export class CreateLinkState extends State { } } } else if (element instanceof PortModel && this.sourcePort && element !== this.sourcePort) { - if ((element instanceof InputOutputPortModel && !isQueryHeaderPort(element))) { + if ((element instanceof InputOutputPortModel)) { if (element.attributes.portType === "IN") { let isDisabled = false; if (element instanceof InputOutputPortModel) { 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..3c83dab2b66 --- /dev/null +++ b/workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNode.ts @@ -0,0 +1,184 @@ +/** + * 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, Query } 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 { findInputNode } from "../../utils/node-utils"; +import { getInputPort, getTargetPortPrefix } from "../../utils/port-utils"; +import { OFFSETS } from "../../utils/constants"; +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 targetMappedPort: InputOutputPortModel; + + public inPort: IntermediatePortModel; + public outPort: IntermediatePortModel; + + public diagnostics: DMDiagnostic[]; + public value: string; + public shouldInitLinks: boolean; + public label: string; + + constructor( + public context: IDataMapperContext, + public query: Query + ) { + super( + NODE_ID, + context, + CLAUSE_CONNECTOR_NODE_TYPE + ); + this.value = query.output; + this.diagnostics = query.diagnostics; + } + + initPorts(): void { + const prevSourcePorts = this.sourcePorts; + this.sourcePorts = []; + 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); + 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.query.inputs.forEach((field) => { + const inputField = field?.split('.').pop(); + const matchedSearch = inputSearch === "" || inputField.toLowerCase().includes(inputSearch.toLowerCase()); + + if (!matchedSearch) return; + + const inputNode = findInputNode(field, this, views, lastViewIndex); + if (inputNode) { + const inputPort = getInputPort(inputNode, field?.replace(/\.\d+/g, '')); + if (!this.sourcePorts.some(port => port.getID() === inputPort.getID())) { + this.sourcePorts.push(inputPort); + } + } + }); + + 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 QueryOutputNode) { + const targetPortPrefix = getTargetPortPrefix(node); + + this.targetMappedPort = node.getPort(`${targetPortPrefix}.${this.query.output}.#.IN`) as InputOutputPortModel; + + if (prevSourcePorts.length !== this.sourcePorts.length || + !prevSourcePorts.every((port, idx) => port.getID() === this.sourcePorts[idx]?.getID())) { + this.shouldInitLinks = true; + } + } + }); + } + } + + initLinks(): void { + if (!this.shouldInitLinks) { + return; + } + + this.sourcePorts.forEach((sourcePort) => { + const inPort = this.inPort; + const lm = new DataMapperLinkModel(undefined, this.diagnostics, true, undefined, true); + + if (sourcePort) { + sourcePort.addLinkedPort(this.inPort); + sourcePort.addLinkedPort(this.targetMappedPort) + + lm.setTargetPort(this.inPort); + lm.setSourcePort(sourcePort); + lm.registerListener({ + selectionChanged(event) { + if (event.isSelected) { + inPort.fireEvent({}, "link-selected"); + sourcePort.fireEvent({}, "link-selected"); + } else { + inPort.fireEvent({}, "link-unselected"); + sourcePort.fireEvent({}, "link-unselected"); + } + }, + }) + this.getModel().addAll(lm); + } + }) + + if (this.targetMappedPort) { + const outPort = this.outPort; + const targetPort = this.targetMappedPort; + + const lm = new DataMapperLinkModel(undefined, this.diagnostics, true, undefined, 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); + } + + 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 hasError(): boolean { + return this.diagnostics?.length > 0; + } +} 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..75432824d36 --- /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; + } +} 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..9e1b3379ff3 --- /dev/null +++ b/workspaces/ballerina/data-mapper/src/components/Diagram/Node/ClauseConnector/ClauseConnectorNodeWidget.tsx @@ -0,0 +1,78 @@ +/** + * 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 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 { renderPortWidget } from "../LinkConnector/LinkConnectorWidgetComponents"; +import { DiagnosticWidget } from "../../Diagnostic/DiagnosticWidget"; +import { useDMExpressionBarStore, useDMQueryClausesPanelStore } 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 diagnostic = node.hasError() ? node.diagnostics[0] : null; + const value = node.value; + + const setIsQueryClausesPanelOpen = useDMQueryClausesPanelStore(state => state.setIsQueryClausesPanelOpen); + const onClickOpenClausePanel = () => { + setIsQueryClausesPanelOpen(true); + }; + + const onClickEdit = () => { + const targetPort = node.targetMappedPort; + setExprBarFocusedPort(targetPort); + }; + + return ( +
+
+ {renderPortWidget(engine, node.inPort, `${node?.value}-input`)} + + {diagnostic && ( + + )} + {renderPortWidget(engine, node.outPort, `${node?.value}-output`)} +
+
+ ); +} 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..8f6e7000fba --- /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"; 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..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({ @@ -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/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/Node/QueryOutput/QueryOutputNode.ts b/workspaces/ballerina/data-mapper/src/components/Diagram/Node/QueryOutput/QueryOutputNode.ts index 83d43bc2d2d..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) { 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) { 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"; diff --git a/workspaces/ballerina/data-mapper/src/components/Diagram/Port/model/InputOutputPortModel.ts b/workspaces/ballerina/data-mapper/src/components/Diagram/Port/model/InputOutputPortModel.ts index 84492494a73..348b189749f 100644 --- a/workspaces/ballerina/data-mapper/src/components/Diagram/Port/model/InputOutputPortModel.ts +++ b/workspaces/ballerina/data-mapper/src/components/Diagram/Port/model/InputOutputPortModel.ts @@ -20,7 +20,7 @@ import { IOType, Mapping } from "@wso2/ballerina-core"; import { DataMapperLinkModel, MappingType } from "../../Link"; import { IntermediatePortModel } from "../IntermediatePort"; -import { createNewMapping } from "../../utils/modification-utils"; +import { createNewMapping, mapWithJoin } from "../../utils/modification-utils"; import { getMappingType, isPendingMappingRequired } from "../../utils/common-utils"; export interface InputOutputPortModelGenerics { @@ -75,6 +75,9 @@ export class InputOutputPortModel extends PortModel mapping.output + ':' + mapping.expression).toString(); const subMappings = model?.subMappings?.map(mapping => (mapping as IOType).id).toString(); + const queryIOs = model?.query ? model.query.inputs.toString() + ':' + model.query.output : ''; const collapsedFields = useDMCollapsedFieldsStore(state => state.fields); // Subscribe to collapsedFields const expandedFields = useDMExpandedFieldsStore(state => state.fields); // Subscribe to expandedFields const { inputSearch, outputSearch } = useDMSearchStore(); @@ -99,7 +100,8 @@ export const useDiagramModel = ( inputSearch, outputSearch, mappings, - subMappings + subMappings, + queryIOs ], queryFn: genModel, networkMode: 'always', 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..1a54644bae1 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"; @@ -77,7 +76,6 @@ export function getMappingType(sourcePort: PortModel, targetPort: PortModel): Ma sourcePort.attributes.field ) { - const targetNode = targetPort.getNode(); if (targetNode instanceof QueryOutputNode && targetNode.outputType.kind !== TypeKind.Array) { return MappingType.ArrayToSingletonAggregate; @@ -103,8 +101,15 @@ export function getMappingType(sourcePort: PortModel, targetPort: PortModel): Ma if (sourceDim > 0) { const dimDelta = sourceDim - targetDim; - if (dimDelta == 0) return MappingType.ArrayToArray; - if (dimDelta > 0) return MappingType.ArrayToSingleton; + if (dimDelta == 0) { + if(targetPort.attributes.portName.endsWith(".#")) { + return MappingType.ArrayJoin; + } + return MappingType.ArrayToArray; + } + if (dimDelta > 0) { + return MappingType.ArrayToSingleton; + } } if ((sourceField.kind !== targetField.kind || @@ -218,7 +223,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 +247,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/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(); 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..b234ba8abc5 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 @@ -22,9 +22,10 @@ import { IDataMapperContext } from "../../../utils/DataMapperContext/DataMapperC import { MappingFindingVisitor } from "../../../visitors/MappingFindingVisitor"; import { traverseNode } from "../../../utils/model-utils"; import { expandArrayFn, getValueType } from "./common-utils"; -import { FnMetadata, FnParams, FnReturnType, Mapping, ResultClauseType } from "@wso2/ballerina-core"; +import { FnMetadata, FnParams, FnReturnType, IntermediateClauseType, Mapping, ResultClauseType } from "@wso2/ballerina-core"; import { getImportTypeInfo, isEnumMember } from "./type-utils"; import { InputNode } from "../Node/Input/InputNode"; +import { useDMQueryClausesPanelStore } from "../../../store/store"; export async function createNewMapping(link: DataMapperLinkModel, modifier?: (expr: string) => string) { const sourcePort = link.getSourcePort(); @@ -172,9 +173,35 @@ 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 mapWithJoin(link: DataMapperLinkModel) { + + const sourcePort = link.getSourcePort(); + if (!sourcePort) { + return; + } + + const sourcePortModel = sourcePort as InputOutputPortModel; + + const { setClauseToAdd, setIsQueryClausesPanelOpen } = useDMQueryClausesPanelStore.getState(); + + setClauseToAdd({ + type: IntermediateClauseType.JOIN, + properties: { + name: sourcePortModel.attributes.field.name + "Item", + type: "var", + expression: sourcePortModel.attributes.fieldFQN, + isOuter: false, + lhsExpression: "", + rhsExpression: "", + } + }); + setIsQueryClausesPanelOpen(true); +} + + export function buildInputAccessExpr(fieldFqn: string): string { // Regular expression to match either quoted strings or non-quoted strings with dots const regex = /"([^"]+)"|'([^"]+)'|([^".]+)/g; 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..ec0b3dd8f7c 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`; diff --git a/workspaces/ballerina/data-mapper/src/store/store.ts b/workspaces/ballerina/data-mapper/src/store/store.ts index 545d518bb8c..4d2dda9406d 100644 --- a/workspaces/ballerina/data-mapper/src/store/store.ts +++ b/workspaces/ballerina/data-mapper/src/store/store.ts @@ -18,6 +18,7 @@ import { create } from "zustand"; import { InputOutputPortModel } from "../components/Diagram/Port"; +import { IntermediateClause } from "@wso2/ballerina-core"; interface SubMappingConfig { isSMConfigPanelOpen: boolean; @@ -162,13 +163,18 @@ export const useDMExpressionBarStore = create((set export interface DataMapperQueryClausesPanelState { isQueryClausesPanelOpen: boolean; setIsQueryClausesPanelOpen: (isQueryClausesPanelOpen: boolean) => void; + clauseToAdd: IntermediateClause; + setClauseToAdd: (clauseToAdd: IntermediateClause) => void; resetQueryClausesPanelStore: () => void; } export const useDMQueryClausesPanelStore = create((set) => ({ isQueryClausesPanelOpen: false, + clauseToAdd: undefined, setIsQueryClausesPanelOpen: (isQueryClausesPanelOpen: boolean) => set({ isQueryClausesPanelOpen }), + setClauseToAdd: (clauseToAdd: IntermediateClause) => set({ clauseToAdd }), resetQueryClausesPanelStore: () => set({ - isQueryClausesPanelOpen: false + isQueryClausesPanelOpen: false, + clauseToAdd: undefined }) })); diff --git a/workspaces/ballerina/data-mapper/src/utils/DataMapperContext/DataMapperContext.ts b/workspaces/ballerina/data-mapper/src/utils/DataMapperContext/DataMapperContext.ts index baa52176f44..0ad7a314d45 100644 --- a/workspaces/ballerina/data-mapper/src/utils/DataMapperContext/DataMapperContext.ts +++ b/workspaces/ballerina/data-mapper/src/utils/DataMapperContext/DataMapperContext.ts @@ -21,10 +21,10 @@ import { View } from "../../components/DataMapper/Views/DataMapperView"; export interface IDataMapperContext { model: ExpandedDMModel; views: View[]; + hasInputsOutputsChanged: boolean; addView: (view: View) => void; applyModifications: (outputId: string, expression: string, viewId: string, name: string) => Promise; addArrayElement: (outputId: string, viewId: string, name: string) => Promise; - hasInputsOutputsChanged: boolean; convertToQuery: (mapping: Mapping, clauseType: ResultClauseType, viewId: string, name: string) => Promise; deleteMapping: (mapping: Mapping, viewId: string) => Promise; deleteSubMapping: (index: number, viewId: string) => Promise; @@ -39,10 +39,10 @@ export class DataMapperContext implements IDataMapperContext { constructor( public model: ExpandedDMModel, public views: View[] = [], + public hasInputsOutputsChanged: boolean = false, public addView: (view: View) => void, public applyModifications: (outputId: string, expression: string, viewId: string, name: string) => Promise, public addArrayElement: (outputId: string, viewId: string, name: string) => Promise, - public hasInputsOutputsChanged: boolean = false, public convertToQuery: (mapping: Mapping, clauseType: ResultClauseType, viewId: string, name: string) => Promise, public deleteMapping: (mapping: Mapping, viewId: string) => Promise, public deleteSubMapping: (index: number, viewId: string) => Promise, 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; }