From d4513ea0373823b7a93f556a8e4f69dcfea7d83b Mon Sep 17 00:00:00 2001 From: Kanushka Gayan Date: Sun, 6 Jul 2025 19:41:22 +0530 Subject: [PATCH 1/6] Add util function to fillter and replace env vars --- common/scripts/env-webpack-helper.js | 60 ++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 common/scripts/env-webpack-helper.js diff --git a/common/scripts/env-webpack-helper.js b/common/scripts/env-webpack-helper.js new file mode 100644 index 00000000000..7e9398e0123 --- /dev/null +++ b/common/scripts/env-webpack-helper.js @@ -0,0 +1,60 @@ +/** + * 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. + */ + +/** + * Creates environment variables object for webpack DefinePlugin with fallback logic: + * 1. Check .env file first for variable values + * 2. If .env declares a variable but has no value, fallback to process.env + * 3. Only define variables that are explicitly declared in .env file + * + * @param {Object} env - Parsed environment variables from .env file (from dotenv.config().parsed) + * @returns {Object} Environment variables object ready for webpack.DefinePlugin + */ +function createEnvDefinePlugin(env) { + + const envKeys = Object.create(null); + const missingVars = []; + + if (env) { + Object.entries(env).forEach(([key, value]) => { + if (value !== undefined && value !== '') { + envKeys[`process.env.${key}`] = JSON.stringify(value); + } + else if (process.env[key] !== undefined && process.env[key] !== '') { + envKeys[`process.env.${key}`] = JSON.stringify(process.env[key]); + } + else { + missingVars.push(key); + } + }); + } + + if (missingVars.length > 0) { + throw new Error( + `Missing required environment variables: ${missingVars.join(', ')}\n` + + `Please provide values in either .env file or runtime environment.\n` + ); + } + + return envKeys; + } + + module.exports = { + createEnvDefinePlugin + }; + \ No newline at end of file From 16226e275c1ee58d4ef7e3229952565bd6596f2e Mon Sep 17 00:00:00 2001 From: Kanushka Gayan Date: Sun, 6 Jul 2025 19:42:32 +0530 Subject: [PATCH 2/6] Refactor env vars handling in extension webpack files and improved error handling --- .../ballerina-extension/webpack.config.js | 26 ++++++------------- workspaces/mi/mi-extension/webpack.config.js | 18 ++++++------- .../wso2-platform-extension/webpack.config.js | 15 ++++++----- 3 files changed, 26 insertions(+), 33 deletions(-) diff --git a/workspaces/ballerina/ballerina-extension/webpack.config.js b/workspaces/ballerina/ballerina-extension/webpack.config.js index 2db9c13e27f..34b000956a7 100644 --- a/workspaces/ballerina/ballerina-extension/webpack.config.js +++ b/workspaces/ballerina/ballerina-extension/webpack.config.js @@ -6,30 +6,20 @@ const path = require('path'); const MergeIntoSingleFile = require('webpack-merge-and-include-globally'); const dotenv = require('dotenv'); const webpack = require('webpack'); +const { createEnvDefinePlugin } = require('../../../common/scripts/env-webpack-helper'); const envPath = path.resolve(__dirname, '.env'); const env = dotenv.config({ path: envPath }).parsed; -function shouldSkipEnvVar(key) { - const pathVariables = ['PATH', 'Path']; - return pathVariables.includes(key); +let envKeys; +try { + envKeys = createEnvDefinePlugin(env); +} catch (error) { + console.error('\n❌ Environment Variable Configuration Error:'); + console.error(error.message); + process.exit(1); } -const filteredProcessEnv = Object.fromEntries( - Object.entries(process.env).filter(([key, value]) => !shouldSkipEnvVar(key)) -); - -const mergedEnv = { ...env, ...filteredProcessEnv }; - -const envKeys = Object.fromEntries( - Object.entries(mergedEnv) - .filter(([key, value]) => key && value !== undefined && value !== '') - .map(([key, value]) => [ - `process.env.${key}`, - JSON.stringify(value), - ]) -); - /** @type {import('webpack').Configuration} */ module.exports = { watch: false, diff --git a/workspaces/mi/mi-extension/webpack.config.js b/workspaces/mi/mi-extension/webpack.config.js index b15b9b75e03..87d959fc78a 100644 --- a/workspaces/mi/mi-extension/webpack.config.js +++ b/workspaces/mi/mi-extension/webpack.config.js @@ -20,22 +20,22 @@ 'use strict'; -const fs = require('fs'); const path = require('path'); const dotenv = require('dotenv'); const webpack = require('webpack'); +const { createEnvDefinePlugin } = require('../../../common/scripts/env-webpack-helper'); const envPath = path.resolve(__dirname, '.env'); const env = dotenv.config({ path: envPath }).parsed; -const mergedEnv = { ...env, ...process.env }; - -const envKeys = Object.fromEntries( - Object.entries(mergedEnv).map(([key, value]) => [ - `process.env.${key}`, - JSON.stringify(value), - ]) -); +let envKeys; +try { + envKeys = createEnvDefinePlugin(env); +} catch (error) { + console.error('\n❌ Environment Variable Configuration Error:'); + console.error(error.message); + process.exit(1); +} /** @type {import('webpack').Configuration} */ module.exports = { diff --git a/workspaces/wso2-platform/wso2-platform-extension/webpack.config.js b/workspaces/wso2-platform/wso2-platform-extension/webpack.config.js index 58cec34bec3..94d7e094a95 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/webpack.config.js +++ b/workspaces/wso2-platform/wso2-platform-extension/webpack.config.js @@ -22,16 +22,19 @@ const CopyPlugin = require("copy-webpack-plugin"); const PermissionsOutputPlugin = require("webpack-permissions-plugin"); const webpack = require("webpack"); const dotenv = require("dotenv"); +const { createEnvDefinePlugin } = require('../../../common/scripts/env-webpack-helper'); const envPath = path.resolve(__dirname, ".env"); const env = dotenv.config({ path: envPath }).parsed; -const mergedEnv = { ...env, ...process.env }; - -const platformEnv = Object.fromEntries(Object.entries(mergedEnv).filter(([key]) => key.startsWith("PLATFORM_"))); - -const envKeys = Object.fromEntries(Object.entries(platformEnv).map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)])); - +let envKeys; +try { + envKeys = createEnvDefinePlugin(env); +} catch (error) { + console.error('\n❌ Environment Variable Configuration Error:'); + console.error(error.message); + process.exit(1); +} //@ts-check /** @typedef {import('webpack').Configuration} WebpackConfig **/ From 97bf0f369ac682284969b451f69c9d114cbe5bf8 Mon Sep 17 00:00:00 2001 From: Kanushka Gayan Date: Mon, 7 Jul 2025 09:57:25 +0530 Subject: [PATCH 3/6] Update error handling for env configuration in webpack files to use warnings instead of errors, allowing builds to continue with empty env vars --- workspaces/ballerina/ballerina-extension/webpack.config.js | 7 ++++--- workspaces/mi/mi-extension/webpack.config.js | 7 ++++--- .../wso2-platform-extension/webpack.config.js | 7 ++++--- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/workspaces/ballerina/ballerina-extension/webpack.config.js b/workspaces/ballerina/ballerina-extension/webpack.config.js index 34b000956a7..2701bc79c22 100644 --- a/workspaces/ballerina/ballerina-extension/webpack.config.js +++ b/workspaces/ballerina/ballerina-extension/webpack.config.js @@ -15,9 +15,10 @@ let envKeys; try { envKeys = createEnvDefinePlugin(env); } catch (error) { - console.error('\n❌ Environment Variable Configuration Error:'); - console.error(error.message); - process.exit(1); + console.warn('\n⚠️ Environment Variable Configuration Warning:'); + console.warn(error.message); + console.warn('Continuing build with empty environment variables...'); + envKeys = {}; } /** @type {import('webpack').Configuration} */ diff --git a/workspaces/mi/mi-extension/webpack.config.js b/workspaces/mi/mi-extension/webpack.config.js index 87d959fc78a..2e7a1f586c5 100644 --- a/workspaces/mi/mi-extension/webpack.config.js +++ b/workspaces/mi/mi-extension/webpack.config.js @@ -32,9 +32,10 @@ let envKeys; try { envKeys = createEnvDefinePlugin(env); } catch (error) { - console.error('\n❌ Environment Variable Configuration Error:'); - console.error(error.message); - process.exit(1); + console.warn('\n⚠️ Environment Variable Configuration Warning:'); + console.warn(error.message); + console.warn('Continuing build with empty environment variables...'); + envKeys = {}; } /** @type {import('webpack').Configuration} */ diff --git a/workspaces/wso2-platform/wso2-platform-extension/webpack.config.js b/workspaces/wso2-platform/wso2-platform-extension/webpack.config.js index 94d7e094a95..eb72aee50b3 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/webpack.config.js +++ b/workspaces/wso2-platform/wso2-platform-extension/webpack.config.js @@ -31,9 +31,10 @@ let envKeys; try { envKeys = createEnvDefinePlugin(env); } catch (error) { - console.error('\n❌ Environment Variable Configuration Error:'); - console.error(error.message); - process.exit(1); + console.warn('\n⚠️ Environment Variable Configuration Warning:'); + console.warn(error.message); + console.warn('Continuing build with empty environment variables...'); + envKeys = {}; } //@ts-check /** @typedef {import('webpack').Configuration} WebpackConfig **/ From 0f8d9b7279fc656e433342526d609fab4a39332c Mon Sep 17 00:00:00 2001 From: madushajg Date: Mon, 7 Jul 2025 13:21:13 +0530 Subject: [PATCH 4/6] Add modified inline data mapper --- .../src/interfaces/extended-lang-client.ts | 75 +++- .../src/interfaces/inline-data-mapper.ts | 136 +++++- .../src/rpc-types/inline-data-mapper/index.ts | 14 +- .../rpc-types/inline-data-mapper/rpc-type.ts | 14 +- .../ballerina-core/src/state-machine-types.ts | 9 +- .../ballerina-extension/src/RPCLayer.ts | 1 + .../src/core/extended-language-client.ts | 31 +- .../inline-data-mapper/rpc-handler.ts | 15 + .../inline-data-mapper/rpc-manager.ts | 131 +++++- .../rpc-managers/inline-data-mapper/utils.ts | 65 +++ .../ballerina-extension/src/stateMachine.ts | 27 +- .../inline-data-mapper/rpc-client.ts | 32 ++ .../src/components/Form/index.tsx | 107 +++-- .../src/components/editors/EditorFactory.tsx | 4 - .../components/editors/ExpressionEditor.tsx | 13 +- .../ballerina-visualizer/src/Hooks.tsx | 23 +- .../ballerina-visualizer/src/MainPanel.tsx | 10 + .../ballerina-visualizer/src/utils/bi.tsx | 21 +- .../Connection/AddConnectionWizard/index.tsx | 9 - .../Connection/EditConnectionWizard/index.tsx | 9 - .../src/views/BI/FlowDiagram/PanelManager.tsx | 15 +- .../src/views/BI/FlowDiagram/index.tsx | 18 +- .../views/BI/Forms/FormGenerator/index.tsx | 8 +- .../views/BI/Forms/FormGeneratorNew/index.tsx | 4 +- .../views/InlineDataMapper/DataMapperView.tsx | 290 +++++++++++++ .../src/views/InlineDataMapper/index.tsx | 128 +----- .../views/InlineDataMapper/modelProcessor.ts | 202 +++++++++ .../ballerina/inline-data-mapper/package.json | 1 - .../src/components/DataMapper/DataMapper.tsx | 156 ++++++- .../DataMapper/Header/AutoMapButton.tsx | 46 ++ .../DataMapper/Header/DataMapperHeader.tsx | 94 ++++- .../DataMapper/Header/EditButton.tsx | 47 +++ .../DataMapper/Header/ExpressionBar.tsx | 2 +- .../DataMapper/Header/HeaderBreadcrumb.tsx | 25 +- .../DataMapper/Header/HeaderSearchBox.tsx | 75 ++-- .../Header/HeaderSearchBoxOptions.tsx | 95 +++++ .../components/DataMapper/Header/utils.tsx | 47 ++- .../SidePanel/QueryClauses/ClauseEditor.tsx | 147 +++++++ .../SidePanel/QueryClauses/ClauseItem.tsx | 140 +++++++ .../SidePanel/QueryClauses/ClausesPanel.tsx | 141 +++++++ .../SidePanel/QueryClauses/styles.tsx | 195 +++++++++ .../SubMappingConfig/SubMappingConfigForm.tsx | 241 +++++++++++ .../DataMapper/Views/DataMapperView.ts | 14 +- .../Actions/IONodesScrollCanvasAction.ts | 7 +- .../src/components/Diagram/Actions/utils.ts | 20 +- .../DataMapperCanvasContainerWidget.tsx | 6 +- .../src/components/Diagram/Diagram.tsx | 17 +- .../Diagram/Label/ExpressionLabelFactory.tsx | 4 + .../Diagram/Label/ExpressionLabelModel.ts | 5 + .../Diagram/Label/ExpressionLabelWidget.tsx | 22 +- .../Diagram/Label/QueryExprLabelWidget.tsx | 123 ++++++ .../Link/DataMapperLink/DataMapperLink.ts | 3 +- .../DefaultLinkSegmentWidget.tsx | 3 +- .../Diagram/LinkState/CreateLinkState.ts | 15 +- .../Diagram/LinkState/DefaultState.ts | 4 +- .../ArrayOutput/ArrayOuptutFieldWidget.tsx | 55 ++- .../Node/ArrayOutput/ArrayOutputNode.ts | 53 ++- .../ArrayOutput/ArrayOutputNodeFactory.tsx | 4 +- .../Node/ArrayOutput/ArrayOutputWidget.tsx | 9 +- .../ArrayOutput/OutputFieldPreviewWidget.tsx | 194 +++++++++ .../EmptyInputs/EmptyInputsNodeWidget.tsx | 9 +- .../Diagram/Node/Input/InputNode.ts | 71 +++- .../Diagram/Node/Input/InputNodeFactory.tsx | 15 +- .../Node/Input/InputNodeTreeItemWidget.tsx | 55 ++- .../Diagram/Node/Input/InputNodeWidget.tsx | 21 +- .../Node/LinkConnector/LinkConnectorNode.ts | 33 +- .../LinkConnector/LinkConnectorNodeWidget.tsx | 7 +- .../ObjectOutput/ObjectOutputFieldWidget.tsx | 18 +- .../Node/ObjectOutput/ObjectOutputNode.ts | 46 +- .../ObjectOutput/ObjectOutputNodeFactory.tsx | 6 +- .../Node/ObjectOutput/ObjectOutputWidget.tsx | 9 +- .../PrimitiveOutputElementWidget.tsx | 9 +- .../PrimitiveOutput/PrimitiveOutputNode.ts | 164 ++++++++ .../PrimitiveOutputNodeFactory.tsx | 59 +++ .../PrimitiveOutput/PrimitiveOutputWidget.tsx | 174 ++++++++ .../Diagram/Node/PrimitiveOutput/index.ts | 19 + .../QueryExprConnectorNode.ts | 225 ++++++++++ .../QueryExprConnectorNodeFactory.tsx | 43 ++ .../QueryExprConnectorNodeWidget.tsx | 111 +++++ .../Diagram/Node/QueryExprConnector/index.ts | 20 + .../Node/QueryOutput/QueryOutputNode.ts | 196 +++++++++ .../QueryOutput/QueryOutputNodeFactory.tsx | 62 +++ .../Node/QueryOutput/QueryOutputWidget.tsx | 190 +++++++++ .../Diagram/Node/QueryOutput/index.ts | 19 + .../Node/SubMapping/SubMappingItemWidget.tsx | 253 +++++++++++ .../Diagram/Node/SubMapping/SubMappingNode.ts | 142 +++++++ .../Node/SubMapping/SubMappingNodeFactory.tsx | 54 +++ .../Node/SubMapping/SubMappingSeparator.tsx | 90 ++++ .../Node/SubMapping/SubMappingTreeWidget.tsx | 114 +++++ .../Diagram/Node/SubMapping/index.ts | 19 + .../Diagram/Node/commons/DataMapperNode.ts | 395 +++++++++++++----- .../Node/commons/PrimitiveTypeInputWidget.tsx | 2 +- .../commons/Search/SearchNoResultFound.tsx | 46 +- .../Diagram/Node/commons/Search/index.tsx | 25 +- .../Diagram/Node/commons/Tree/Tree.tsx | 10 +- .../ValueConfigButton/ValueConfigMenu.tsx | 1 + .../src/components/Diagram/Node/index.ts | 4 + .../Port/model/InputOutputPortModel.ts | 68 ++- .../components/Diagram/utils/common-utils.ts | 101 +++-- .../src/components/Diagram/utils/constants.ts | 11 +- .../components/Diagram/utils/diagram-utils.ts | 18 +- .../components/Diagram/utils/link-utils.ts | 4 +- .../Diagram/utils/modification-utils.ts | 129 +----- .../components/Diagram/utils/port-utils.ts | 20 +- .../components/Diagram/utils/search-utils.ts | 19 +- .../src/components/Hooks/index.tsx | 35 +- .../src/components/styles.ts | 77 +++- .../inline-data-mapper/src/index.tsx | 15 +- .../inline-data-mapper/src/store/store.ts | 55 +++ .../DataMapperContext/DataMapperContext.ts | 19 +- .../src/utils/model-utils.ts | 40 +- .../src/visitors/BaseVisitor.ts | 21 +- .../src/visitors/IONodeInitVisitor.ts | 63 +++ .../visitors/IntermediateNodeInitVisitor.ts | 75 ++++ .../src/visitors/SubMappingNodeInitVisitor.ts | 41 ++ .../font-wso2-vscode/src/icons/map-array.svg | 13 + .../SidePanel/SidePanel.stories.tsx | 55 +++ .../src/components/SidePanel/SidePanel.tsx | 2 +- 118 files changed, 6132 insertions(+), 906 deletions(-) create mode 100644 workspaces/ballerina/ballerina-extension/src/rpc-managers/inline-data-mapper/utils.ts create mode 100644 workspaces/ballerina/ballerina-visualizer/src/views/InlineDataMapper/DataMapperView.tsx create mode 100644 workspaces/ballerina/ballerina-visualizer/src/views/InlineDataMapper/modelProcessor.ts create mode 100644 workspaces/ballerina/inline-data-mapper/src/components/DataMapper/Header/AutoMapButton.tsx create mode 100644 workspaces/ballerina/inline-data-mapper/src/components/DataMapper/Header/EditButton.tsx create mode 100644 workspaces/ballerina/inline-data-mapper/src/components/DataMapper/Header/HeaderSearchBoxOptions.tsx create mode 100644 workspaces/ballerina/inline-data-mapper/src/components/DataMapper/SidePanel/QueryClauses/ClauseEditor.tsx create mode 100644 workspaces/ballerina/inline-data-mapper/src/components/DataMapper/SidePanel/QueryClauses/ClauseItem.tsx create mode 100644 workspaces/ballerina/inline-data-mapper/src/components/DataMapper/SidePanel/QueryClauses/ClausesPanel.tsx create mode 100644 workspaces/ballerina/inline-data-mapper/src/components/DataMapper/SidePanel/QueryClauses/styles.tsx create mode 100644 workspaces/ballerina/inline-data-mapper/src/components/DataMapper/SidePanel/SubMappingConfig/SubMappingConfigForm.tsx create mode 100644 workspaces/ballerina/inline-data-mapper/src/components/Diagram/Label/QueryExprLabelWidget.tsx create mode 100644 workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/ArrayOutput/OutputFieldPreviewWidget.tsx create mode 100644 workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/PrimitiveOutput/PrimitiveOutputNode.ts create mode 100644 workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/PrimitiveOutput/PrimitiveOutputNodeFactory.tsx create mode 100644 workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/PrimitiveOutput/PrimitiveOutputWidget.tsx create mode 100644 workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/PrimitiveOutput/index.ts create mode 100644 workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/QueryExprConnector/QueryExprConnectorNode.ts create mode 100644 workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/QueryExprConnector/QueryExprConnectorNodeFactory.tsx create mode 100644 workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/QueryExprConnector/QueryExprConnectorNodeWidget.tsx create mode 100644 workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/QueryExprConnector/index.ts create mode 100644 workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/QueryOutput/QueryOutputNode.ts create mode 100644 workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/QueryOutput/QueryOutputNodeFactory.tsx create mode 100644 workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/QueryOutput/QueryOutputWidget.tsx create mode 100644 workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/QueryOutput/index.ts create mode 100644 workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/SubMapping/SubMappingItemWidget.tsx create mode 100644 workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/SubMapping/SubMappingNode.ts create mode 100644 workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/SubMapping/SubMappingNodeFactory.tsx create mode 100644 workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/SubMapping/SubMappingSeparator.tsx create mode 100644 workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/SubMapping/SubMappingTreeWidget.tsx create mode 100644 workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/SubMapping/index.ts create mode 100644 workspaces/ballerina/inline-data-mapper/src/visitors/IONodeInitVisitor.ts create mode 100644 workspaces/ballerina/inline-data-mapper/src/visitors/IntermediateNodeInitVisitor.ts create mode 100644 workspaces/ballerina/inline-data-mapper/src/visitors/SubMappingNodeInitVisitor.ts create mode 100644 workspaces/common-libs/font-wso2-vscode/src/icons/map-array.svg diff --git a/workspaces/ballerina/ballerina-core/src/interfaces/extended-lang-client.ts b/workspaces/ballerina/ballerina-core/src/interfaces/extended-lang-client.ts index 1e0046f1c78..ff6680b2ed7 100644 --- a/workspaces/ballerina/ballerina-core/src/interfaces/extended-lang-client.ts +++ b/workspaces/ballerina/ballerina-core/src/interfaces/extended-lang-client.ts @@ -28,7 +28,7 @@ import { ConnectorRequest, ConnectorResponse } from "../rpc-types/connector-wiza import { SqFlow } from "../rpc-types/sequence-diagram/interfaces"; import { FieldType, FunctionModel, ListenerModel, ServiceClassModel, ServiceModel } from "./service"; import { CDModel } from "./component-diagram"; -import { IDMModel, Mapping } from "./inline-data-mapper"; +import { DMModel, ExpandedDMModel, IntermediateClause, Mapping, Query } from "./inline-data-mapper"; import { SCOPE } from "../state-machine-types"; export interface DidOpenParams { @@ -279,15 +279,30 @@ export interface TypeWithIdentifier { type: TypeField; } -export interface InlineDataMapperModelRequest { +export interface InitialIDMSourceRequest { filePath: string; flowNode: FlowNode; - propertyKey: string; +} + +export interface InitialIDMSourceResponse { + textEdits: { + [key: string]: TextEdit[]; + }; +} + +export interface InlineDataMapperModelRequest { + filePath: string; + codedata: CodeData; position: LinePosition; + targetField?: string; } -export interface InlineDataMapperSourceRequest extends InlineDataMapperModelRequest { - mappings: Mapping[]; +export interface InlineDataMapperSourceRequest { + filePath: string; + codedata: CodeData; + varName?: string; + targetField?: string; + mapping: Mapping; } export interface VisualizableFieldsRequest { @@ -297,23 +312,61 @@ export interface VisualizableFieldsRequest { } export interface InlineDataMapperModelResponse { - mappingsModel: IDMModel; + mappingsModel: ExpandedDMModel | DMModel; } export interface InlineDataMapperSourceResponse { - source: string; + textEdits: { + [key: string]: TextEdit[]; + }; } export interface VisualizableFieldsResponse { - visualizableProperties: string[]; + visualizableProperties: { + [key: string]: string; + }; } export interface AddArrayElementRequest { filePath: string; - flowNode: FlowNode; - position: LinePosition; - propertyKey: string; + codedata: CodeData; + varName?: string; + targetField?: string; + propertyKey?: string; +} + +export interface ConvertToQueryRequest{ + filePath: string; + codedata: CodeData; + varName?: string; + targetField?: string; + propertyKey?: string; +} + +export interface AddClausesRequest { + filePath: string; + codedata: CodeData; + index: number; + clause: IntermediateClause; + varName?: string; targetField: string; + propertyKey?: string; +} + +export interface GetInlineDataMapperCodedataRequest { + filePath: string; + codedata: CodeData; + name: string; +} + +export interface GetSubMappingCodedataRequest { + filePath: string; + codedata: CodeData; + view: string; +} + +export interface GetInlineDataMapperCodedataResponse { + codedata: CodeData; } export interface GraphqlDesignServiceParams { diff --git a/workspaces/ballerina/ballerina-core/src/interfaces/inline-data-mapper.ts b/workspaces/ballerina/ballerina-core/src/interfaces/inline-data-mapper.ts index a65e2cdfb80..08f9e094a6d 100644 --- a/workspaces/ballerina/ballerina-core/src/interfaces/inline-data-mapper.ts +++ b/workspaces/ballerina/ballerina-core/src/interfaces/inline-data-mapper.ts @@ -16,6 +16,9 @@ * under the License. */ +import { CodeData } from "./bi"; +import { LineRange } from "./common"; + export enum TypeKind { Record = "record", Array = "array", @@ -24,13 +27,23 @@ export enum TypeKind { Float = "float", Decimal = "decimal", Boolean = "boolean", + Enum = "enum", Unknown = "unknown" } export enum InputCategory { Const = "const", ModuleVariable = "moduleVariable", - Configurable = "configurable" + Configurable = "configurable", + Enum = "enum" +} + +export enum IntermediateClauseType { + LET = "let", + WHERE = "where", + FROM = "from", + ORDER_BY = "order by", + LIMIT = "limit" } export interface IDMDiagnostic { @@ -56,28 +69,145 @@ export interface IOType { variableName?: string; fields?: IOType[]; member?: IOType; + members?: EnumMember[]; defaultValue?: unknown; optional?: boolean; } export interface Mapping { output: string, - inputs: string[]; + inputs?: string[]; expression: string; elements?: MappingElement[]; diagnostics?: IDMDiagnostic[]; isComplex?: boolean; isFunctionCall?: boolean; + isQueryExpression?: boolean; } -export interface IDMModel { +export interface ExpandedDMModel { inputs: IOType[]; output: IOType; + subMappings?: IOType[]; mappings: Mapping[]; source: string; view: string; + query?: Query; +} + +export interface DMModel { + inputs: IORoot[]; + output: IORoot; + subMappings?: IORoot[]; + types: Record; + mappings: Mapping[]; + view: string; + query?: Query; +} + +export interface ModelState { + model: ExpandedDMModel; + hasInputsOutputsChanged?: boolean; + hasSubMappingsChanged?: boolean; +} + +export interface IORoot extends IOTypeField { + id: string; + category?: InputCategory; +} + +export interface RecordType { + fields: IOTypeField[]; +} + +export interface EnumType { + members?: EnumMember[]; +} + +export interface IOTypeField { + typeName?: string; + kind: TypeKind; + fieldName?: string; + member?: IOTypeField; + defaultValue?: unknown; + optional?: boolean; + ref?: string; +} + +export interface EnumMember { + id: string; + value: string; } export interface MappingElement { mappings: Mapping[]; } + +export interface Query { + output: string, + inputs: string[]; + diagnostics?: IDMDiagnostic[]; + fromClause: FromClause; + intermediateClauses?: IntermediateClause[]; + resultClause: string; +} + +export interface FromClause { + name: string; + type: string; + expression: string; +} + +export interface IntermediateClauseProps { + name?: string; + type?: string; + expression: string; + order?: "ascending" | "descending"; +} + +export interface IntermediateClause { + type: IntermediateClauseType; + properties: IntermediateClauseProps; +} + +export interface ResultClause { + type: string; + properties: { + expression: string; + }; + query?: Query; +} + +export interface IDMFormProps { + targetLineRange: LineRange; + fields: IDMFormField[]; + submitText?: string; + cancelText?: string; + nestedForm?: boolean; + onSubmit: (data: IDMFormFieldValues) => void; + onCancel?: () => void; + isSaving?: boolean; + helperPaneSide?: 'right' | 'left'; +} + +export interface IDMFormField { + key: string; + label: string; + type: null | string; + optional: boolean; + editable: boolean; + documentation: string; + value: string | any[]; + valueTypeConstraint: string; + enabled: boolean; + items?: string[]; +} + +export interface IDMFormFieldValues { + [key: string]: any; +} + +export interface IDMViewState { + viewId: string; + codedata?: CodeData; +} diff --git a/workspaces/ballerina/ballerina-core/src/rpc-types/inline-data-mapper/index.ts b/workspaces/ballerina/ballerina-core/src/rpc-types/inline-data-mapper/index.ts index 6a6616b3483..7418d57f78d 100644 --- a/workspaces/ballerina/ballerina-core/src/rpc-types/inline-data-mapper/index.ts +++ b/workspaces/ballerina/ballerina-core/src/rpc-types/inline-data-mapper/index.ts @@ -17,17 +17,29 @@ */ import { AddArrayElementRequest, + ConvertToQueryRequest, + AddClausesRequest, InlineDataMapperModelRequest, InlineDataMapperModelResponse, InlineDataMapperSourceRequest, InlineDataMapperSourceResponse, VisualizableFieldsRequest, - VisualizableFieldsResponse + VisualizableFieldsResponse, + InitialIDMSourceResponse, + InitialIDMSourceRequest, + GetInlineDataMapperCodedataRequest, + GetInlineDataMapperCodedataResponse, + GetSubMappingCodedataRequest } from "../../interfaces/extended-lang-client"; export interface InlineDataMapperAPI { + getInitialIDMSource: (params: InitialIDMSourceRequest) => Promise; getDataMapperModel: (params: InlineDataMapperModelRequest) => Promise; getDataMapperSource: (params: InlineDataMapperSourceRequest) => Promise; getVisualizableFields: (params: VisualizableFieldsRequest) => Promise; addNewArrayElement: (params: AddArrayElementRequest) => Promise; + convertToQuery: (params: ConvertToQueryRequest) => Promise; + addClauses: (params: AddClausesRequest) => Promise; + getDataMapperCodedata: (params: GetInlineDataMapperCodedataRequest) => Promise; + getSubMappingCodedata: (params: GetSubMappingCodedataRequest) => Promise; } diff --git a/workspaces/ballerina/ballerina-core/src/rpc-types/inline-data-mapper/rpc-type.ts b/workspaces/ballerina/ballerina-core/src/rpc-types/inline-data-mapper/rpc-type.ts index 4855f787af7..330d6b2f56a 100644 --- a/workspaces/ballerina/ballerina-core/src/rpc-types/inline-data-mapper/rpc-type.ts +++ b/workspaces/ballerina/ballerina-core/src/rpc-types/inline-data-mapper/rpc-type.ts @@ -19,17 +19,29 @@ */ import { AddArrayElementRequest, + ConvertToQueryRequest, + AddClausesRequest, InlineDataMapperModelRequest, InlineDataMapperModelResponse, InlineDataMapperSourceRequest, InlineDataMapperSourceResponse, VisualizableFieldsRequest, - VisualizableFieldsResponse + VisualizableFieldsResponse, + InitialIDMSourceResponse, + InitialIDMSourceRequest, + GetInlineDataMapperCodedataRequest, + GetInlineDataMapperCodedataResponse, + GetSubMappingCodedataRequest } from "../../interfaces/extended-lang-client"; import { RequestType } from "vscode-messenger-common"; const _preFix = "inline-data-mapper"; +export const getInitialIDMSource: RequestType = { method: `${_preFix}/getInitialIDMSource` }; export const getDataMapperModel: RequestType = { method: `${_preFix}/getDataMapperModel` }; export const getDataMapperSource: RequestType = { method: `${_preFix}/getDataMapperSource` }; export const getVisualizableFields: RequestType = { method: `${_preFix}/getVisualizableFields` }; export const addNewArrayElement: RequestType = { method: `${_preFix}/addNewArrayElement` }; +export const convertToQuery: RequestType = { method: `${_preFix}/convertToQuery` }; +export const addClauses: RequestType = { method: `${_preFix}/addClauses` }; +export const getDataMapperCodedata: RequestType = { method: `${_preFix}/getDataMapperCodedata` }; +export const getSubMappingCodedata: RequestType = { method: `${_preFix}/getSubMappingCodedata` }; diff --git a/workspaces/ballerina/ballerina-core/src/state-machine-types.ts b/workspaces/ballerina/ballerina-core/src/state-machine-types.ts index 74d743380eb..e24a728e79d 100644 --- a/workspaces/ballerina/ballerina-core/src/state-machine-types.ts +++ b/workspaces/ballerina/ballerina-core/src/state-machine-types.ts @@ -20,7 +20,7 @@ import { NotificationType, RequestType } from "vscode-messenger-common"; import { NodePosition, STNode } from "@wso2/syntax-tree"; import { LinePosition } from "./interfaces/common"; import { Type } from "./interfaces/extended-lang-client"; -import { DIRECTORY_MAP, ProjectStructureArtifactResponse, ProjectStructureResponse } from "./interfaces/bi"; +import { CodeData, DIRECTORY_MAP, ProjectStructureArtifactResponse, ProjectStructureResponse } from "./interfaces/bi"; export type MachineStateValue = | 'initialize' @@ -66,6 +66,7 @@ export enum MACHINE_VIEW { ServiceDesigner = "Service Designer", ERDiagram = "ER Diagram", DataMapper = "Data Mapper", + InlineDataMapper = "Inline Data Mapper", GraphQLDiagram = "GraphQL Diagram", TypeDiagram = "Type Diagram", SetupView = "Setup View", @@ -126,6 +127,7 @@ export interface VisualizerLocation { projectStructure?: ProjectStructureResponse; org?: string; package?: string; + dataMapperMetadata?: DataMapperMetadata; } export interface ArtifactData { @@ -141,6 +143,11 @@ export interface VisualizerMetadata { target?: LinePosition; } +export interface DataMapperMetadata { + name: string; + codeData: CodeData; +} + export interface PopupVisualizerLocation extends VisualizerLocation { recentIdentifier?: string; artifactType?: DIRECTORY_MAP; diff --git a/workspaces/ballerina/ballerina-extension/src/RPCLayer.ts b/workspaces/ballerina/ballerina-extension/src/RPCLayer.ts index 6326f6c0cc3..f6cab3a464e 100644 --- a/workspaces/ballerina/ballerina-extension/src/RPCLayer.ts +++ b/workspaces/ballerina/ballerina-extension/src/RPCLayer.ts @@ -142,6 +142,7 @@ async function getContext(): Promise { scope: context.scope, org: context.org, package: context.package, + dataMapperMetadata: context.dataMapperMetadata }); }); } diff --git a/workspaces/ballerina/ballerina-extension/src/core/extended-language-client.ts b/workspaces/ballerina/ballerina-extension/src/core/extended-language-client.ts index f6d845902be..a734f0628d9 100644 --- a/workspaces/ballerina/ballerina-extension/src/core/extended-language-client.ts +++ b/workspaces/ballerina/ballerina-extension/src/core/extended-language-client.ts @@ -167,6 +167,7 @@ import { VisualizableFieldsRequest, VisualizableFieldsResponse, AddArrayElementRequest, + ConvertToQueryRequest, GetTestFunctionRequest, GetTestFunctionResponse, AddOrUpdateTestFunctionRequest, @@ -217,6 +218,7 @@ import { MemoryManagersRequest, MemoryManagersResponse, ArtifactsNotification, + AddClausesRequest, OpenConfigTomlRequest, UpdateConfigVariableRequestV2, GetConfigVariableNodeTemplateRequest, @@ -224,7 +226,10 @@ import { DeleteConfigVariableRequestV2, DeleteConfigVariableResponseV2, JsonToTypeRequest, - JsonToTypeResponse + JsonToTypeResponse, + GetInlineDataMapperCodedataRequest, + GetInlineDataMapperCodedataResponse, + GetSubMappingCodedataRequest } from "@wso2/ballerina-core"; import { BallerinaExtension } from "./index"; import { debug, handlePullModuleProgress } from "../utils"; @@ -300,6 +305,10 @@ enum EXTENDED_APIS { DATA_MAPPER_GET_SOURCE = 'dataMapper/getSource', DATA_MAPPER_VISUALIZABLE = 'dataMapper/visualizable', DATA_MAPPER_ADD_ELEMENT = 'dataMapper/addElement', + DATA_MAPPER_CONVERT_TO_QUERY = 'dataMapper/convertToQuery', + DATA_MAPPER_ADD_CLAUSES = 'dataMapper/addClauses', + DATA_MAPPER_CODEDATA = 'dataMapper/nodePosition', + DATA_MAPPER_SUB_MAPPING_CODEDATA = 'dataMapper/subMapping', VIEW_CONFIG_VARIABLES = 'configEditor/getConfigVariables', UPDATE_CONFIG_VARIABLES = 'configEditor/updateConfigVariables', VIEW_CONFIG_VARIABLES_V2 = 'configEditorV2/getConfigVariables', @@ -656,7 +665,7 @@ export class ExtendedLangClient extends LanguageClient implements ExtendedLangCl return this.sendRequest(EXTENDED_APIS.DATA_MAPPER_MAPPINGS, params); } - async getInlineDataMapperSource(params: InlineDataMapperSourceRequest): Promise { + async getInlineDataMapperSource(params: InlineDataMapperSourceRequest): Promise { return this.sendRequest(EXTENDED_APIS.DATA_MAPPER_GET_SOURCE, params); } @@ -664,10 +673,26 @@ export class ExtendedLangClient extends LanguageClient implements ExtendedLangCl return this.sendRequest(EXTENDED_APIS.DATA_MAPPER_VISUALIZABLE, params); } - async addArrayElement(params: AddArrayElementRequest): Promise { + async addArrayElement(params: AddArrayElementRequest): Promise { return this.sendRequest(EXTENDED_APIS.DATA_MAPPER_ADD_ELEMENT, params); } + async convertToQuery(params: ConvertToQueryRequest): Promise { + return this.sendRequest(EXTENDED_APIS.DATA_MAPPER_CONVERT_TO_QUERY, params); + } + + async addClauses(params: AddClausesRequest): Promise { + return this.sendRequest(EXTENDED_APIS.DATA_MAPPER_ADD_CLAUSES, params); + } + + async getDataMapperCodedata(params: GetInlineDataMapperCodedataRequest): Promise { + return this.sendRequest(EXTENDED_APIS.DATA_MAPPER_CODEDATA, params); + } + + async getSubMappingCodedata(params: GetSubMappingCodedataRequest): Promise { + return this.sendRequest(EXTENDED_APIS.DATA_MAPPER_SUB_MAPPING_CODEDATA, params); + } + async getGraphqlModel(params: GraphqlDesignServiceParams): Promise { return this.sendRequest(EXTENDED_APIS.GRAPHQL_DESIGN_MODEL, params); } diff --git a/workspaces/ballerina/ballerina-extension/src/rpc-managers/inline-data-mapper/rpc-handler.ts b/workspaces/ballerina/ballerina-extension/src/rpc-managers/inline-data-mapper/rpc-handler.ts index c3290f00420..867f2d4c9bb 100644 --- a/workspaces/ballerina/ballerina-extension/src/rpc-managers/inline-data-mapper/rpc-handler.ts +++ b/workspaces/ballerina/ballerina-extension/src/rpc-managers/inline-data-mapper/rpc-handler.ts @@ -19,12 +19,22 @@ */ import { AddArrayElementRequest, + AddClausesRequest, + ConvertToQueryRequest, + GetInlineDataMapperCodedataRequest, + GetSubMappingCodedataRequest, + InitialIDMSourceRequest, InlineDataMapperModelRequest, InlineDataMapperSourceRequest, VisualizableFieldsRequest, + addClauses, addNewArrayElement, + convertToQuery, + getDataMapperCodedata, getDataMapperModel, getDataMapperSource, + getInitialIDMSource, + getSubMappingCodedata, getVisualizableFields } from "@wso2/ballerina-core"; import { Messenger } from "vscode-messenger"; @@ -32,8 +42,13 @@ import { InlineDataMapperRpcManager } from "./rpc-manager"; export function registerInlineDataMapperRpcHandlers(messenger: Messenger) { const rpcManger = new InlineDataMapperRpcManager(); + messenger.onRequest(getInitialIDMSource, (args: InitialIDMSourceRequest) => rpcManger.getInitialIDMSource(args)); messenger.onRequest(getDataMapperModel, (args: InlineDataMapperModelRequest) => rpcManger.getDataMapperModel(args)); messenger.onRequest(getDataMapperSource, (args: InlineDataMapperSourceRequest) => rpcManger.getDataMapperSource(args)); messenger.onRequest(getVisualizableFields, (args: VisualizableFieldsRequest) => rpcManger.getVisualizableFields(args)); messenger.onRequest(addNewArrayElement, (args: AddArrayElementRequest) => rpcManger.addNewArrayElement(args)); + messenger.onRequest(convertToQuery, (args: ConvertToQueryRequest) => rpcManger.convertToQuery(args)); + messenger.onRequest(addClauses, (args: AddClausesRequest) => rpcManger.addClauses(args)); + messenger.onRequest(getDataMapperCodedata, (args: GetInlineDataMapperCodedataRequest) => rpcManger.getDataMapperCodedata(args)); + messenger.onRequest(getSubMappingCodedata, (args: GetSubMappingCodedataRequest) => rpcManger.getSubMappingCodedata(args)); } diff --git a/workspaces/ballerina/ballerina-extension/src/rpc-managers/inline-data-mapper/rpc-manager.ts b/workspaces/ballerina/ballerina-extension/src/rpc-managers/inline-data-mapper/rpc-manager.ts index a9e9dce6664..020e8d124e0 100644 --- a/workspaces/ballerina/ballerina-extension/src/rpc-managers/inline-data-mapper/rpc-manager.ts +++ b/workspaces/ballerina/ballerina-extension/src/rpc-managers/inline-data-mapper/rpc-manager.ts @@ -19,18 +19,77 @@ */ import { AddArrayElementRequest, + AddClausesRequest, + ConvertToQueryRequest, + EVENT_TYPE, + GetInlineDataMapperCodedataRequest, + GetInlineDataMapperCodedataResponse, + GetSubMappingCodedataRequest, + InitialIDMSourceRequest, + InitialIDMSourceResponse, InlineDataMapperAPI, InlineDataMapperModelRequest, InlineDataMapperModelResponse, InlineDataMapperSourceRequest, InlineDataMapperSourceResponse, + MACHINE_VIEW, VisualizableFieldsRequest, VisualizableFieldsResponse } from "@wso2/ballerina-core"; -import { StateMachine } from "../../stateMachine"; +import { openView, StateMachine } from "../../stateMachine"; +import { updateSourceCode } from "../../utils"; +import { fetchDataMapperCodeData, updateAndRefreshDataMapper } from "./utils"; export class InlineDataMapperRpcManager implements InlineDataMapperAPI { + async getInitialIDMSource(params: InitialIDMSourceRequest): Promise { + console.log(">>> requesting inline data mapper initial source from ls", params); + return new Promise((resolve) => { + StateMachine.langClient() + .getSourceCode(params) + .then(async (model) => { + console.log(">>> inline data mapper initial source from ls", model); + await updateSourceCode({ textEdits: model.textEdits }); + + let modelCodeData = params.flowNode.codedata; + if (modelCodeData.isNew) { + // Clone the object to avoid mutating the original reference + const clonedModelCodeData = { ...modelCodeData }; + clonedModelCodeData.lineRange.startLine.line+=1; + clonedModelCodeData.lineRange.endLine.line+=1; + modelCodeData = clonedModelCodeData; + } + + const varName = params.flowNode.properties?.variable?.value as string ?? null; + const codeData = await fetchDataMapperCodeData(params.filePath, modelCodeData, varName); + // const codeData = params.flowNode.codedata; + // const varName = params.flowNode.properties?.variable?.value as string ?? null; + + openView(EVENT_TYPE.OPEN_VIEW, { + view: MACHINE_VIEW.InlineDataMapper, + documentUri: params.filePath, + position: { + startLine: codeData.lineRange.startLine.line, + startColumn: codeData.lineRange.startLine.offset, + endLine: codeData.lineRange.endLine.line, + endColumn: codeData.lineRange.endLine.offset + }, + dataMapperMetadata: { + name: varName, + codeData: codeData + } + }); + resolve({ textEdits: model.textEdits }); + }) + .catch((error) => { + console.log(">>> error fetching inline data mapper initial source from ls", error); + return new Promise((resolve) => { + resolve({ artifacts: [], error: error }); + }); + }); + }); + } + async getDataMapperModel(params: InlineDataMapperModelRequest): Promise { return new Promise(async (resolve) => { const dataMapperModel = await StateMachine @@ -43,11 +102,13 @@ export class InlineDataMapperRpcManager implements InlineDataMapperAPI { async getDataMapperSource(params: InlineDataMapperSourceRequest): Promise { return new Promise(async (resolve) => { - const dataMapperSource = await StateMachine - .langClient() - .getInlineDataMapperSource(params) as InlineDataMapperSourceResponse; - - resolve(dataMapperSource); + StateMachine.langClient() + .getInlineDataMapperSource(params) + .then(async (resp) => { + console.log(">>> inline data mapper initial source from ls", resp); + updateAndRefreshDataMapper(resp.textEdits, params.filePath, params.codedata, params.varName); + resolve({ textEdits: resp.textEdits }); + }); }); } @@ -63,11 +124,63 @@ export class InlineDataMapperRpcManager implements InlineDataMapperAPI { async addNewArrayElement(params: AddArrayElementRequest): Promise { return new Promise(async (resolve) => { - const dataMapperSource = await StateMachine + await StateMachine.langClient() + .addArrayElement({ + filePath: params.filePath, + codedata: params.codedata, + targetField: params.targetField, + propertyKey: params.propertyKey + }) + .then(async (resp) => { + console.log(">>> inline data mapper add array element response", resp); + updateAndRefreshDataMapper(resp.textEdits, params.filePath, params.codedata, params.varName); + resolve({ textEdits: resp.textEdits }); + }); + }); + } + + async convertToQuery(params: ConvertToQueryRequest): Promise { + return new Promise(async (resolve) => { + await StateMachine.langClient() + .convertToQuery(params) + .then(async (resp) => { + console.log(">>> inline data mapper convert to query response", resp); + updateAndRefreshDataMapper(resp.textEdits, params.filePath, params.codedata, params.varName); + resolve({ textEdits: resp.textEdits }); + }); + }); + } + + async addClauses(params: AddClausesRequest): Promise { + return new Promise(async (resolve) => { + await StateMachine + .langClient() + .addClauses(params) + .then(async (resp) => { + console.log(">>> inline data mapper add clauses response", resp); + updateAndRefreshDataMapper(resp.textEdits, params.filePath, params.codedata, params.varName); + resolve({ textEdits: resp.textEdits }); + }); + }); + } + + async getDataMapperCodedata(params: GetInlineDataMapperCodedataRequest): Promise { + return new Promise(async (resolve) => { + const dataMapperCodedata = await StateMachine + .langClient() + .getDataMapperCodedata(params) as GetInlineDataMapperCodedataResponse; + + resolve(dataMapperCodedata); + }); + } + + async getSubMappingCodedata(params: GetSubMappingCodedataRequest): Promise { + return new Promise(async (resolve) => { + const dataMapperCodedata = await StateMachine .langClient() - .addArrayElement(params) as InlineDataMapperSourceResponse; + .getSubMappingCodedata(params) as GetInlineDataMapperCodedataResponse; - resolve(dataMapperSource); + resolve(dataMapperCodedata); }); } } diff --git a/workspaces/ballerina/ballerina-extension/src/rpc-managers/inline-data-mapper/utils.ts b/workspaces/ballerina/ballerina-extension/src/rpc-managers/inline-data-mapper/utils.ts new file mode 100644 index 00000000000..375010049b7 --- /dev/null +++ b/workspaces/ballerina/ballerina-extension/src/rpc-managers/inline-data-mapper/utils.ts @@ -0,0 +1,65 @@ +/** + * 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 { CodeData, TextEdit } from "@wso2/ballerina-core"; +import { updateSourceCode } from "../../utils"; +import { StateMachine, updateInlineDataMapperView } from "../../stateMachine"; + +/** + * Applies text edits to the source code. + */ +export async function applyTextEdits(textEdits: { [key: string]: TextEdit[] }) { + await updateSourceCode({ textEdits }); +} + +/** + * Fetches the latest code data for the data mapper. + */ +export async function fetchDataMapperCodeData( + filePath: string, + codedata: CodeData, + varName: string +): Promise { + const response = await StateMachine + .langClient() + .getDataMapperCodedata({ filePath, codedata, name: varName }); + return response.codedata; +} + +/** + * Orchestrates the update and refresh process for the data mapper. + */ +export async function updateAndRefreshDataMapper( + textEdits: { [key: string]: TextEdit[] }, + filePath: string, + codedata: CodeData, + varName: string +) { + await applyTextEdits(textEdits); + const newCodeData = await fetchDataMapperCodeData(filePath, codedata, varName); + + // Hack to update the codedata with the new source code + // TODO: Remove this once the lang server is updated to return the new source code + if (newCodeData) { + const newSrc = Array.isArray(Object.values(textEdits)) + ? Object.values(textEdits)[0][0].newText + : Math.random().toString(36).substring(2) + Date.now().toString(36); + newCodeData.sourceCode = newSrc; + } + + updateInlineDataMapperView(newCodeData); +} diff --git a/workspaces/ballerina/ballerina-extension/src/stateMachine.ts b/workspaces/ballerina/ballerina-extension/src/stateMachine.ts index ed49d2e4cbb..0145bc5c630 100644 --- a/workspaces/ballerina/ballerina-extension/src/stateMachine.ts +++ b/workspaces/ballerina/ballerina-extension/src/stateMachine.ts @@ -2,7 +2,7 @@ import { ExtendedLangClient } from './core'; import { createMachine, assign, interpret } from 'xstate'; import { activateBallerina } from './extension'; -import { EVENT_TYPE, SyntaxTree, History, HistoryEntry, MachineStateValue, STByRangeRequest, SyntaxTreeResponse, UndoRedoManager, VisualizerLocation, webviewReady, MACHINE_VIEW, DIRECTORY_MAP, SCOPE, ProjectStructureResponse, ArtifactData, ProjectStructureArtifactResponse } from "@wso2/ballerina-core"; +import { EVENT_TYPE, SyntaxTree, History, HistoryEntry, MachineStateValue, STByRangeRequest, SyntaxTreeResponse, UndoRedoManager, VisualizerLocation, webviewReady, MACHINE_VIEW, DIRECTORY_MAP, SCOPE, ProjectStructureResponse, ArtifactData, ProjectStructureArtifactResponse, CodeData } from "@wso2/ballerina-core"; import { fetchAndCacheLibraryData } from './features/library-browser'; import { VisualizerWebview } from './views/visualizer/webview'; import { commands, extensions, Uri, window, workspace, WorkspaceFolder } from 'vscode'; @@ -128,7 +128,8 @@ const stateMachine = createMachine( type: (context, event) => event.viewLocation?.type, isGraphql: (context, event) => event.viewLocation?.isGraphql, metadata: (context, event) => event.viewLocation?.metadata, - addType: (context, event) => event.viewLocation?.addType + addType: (context, event) => event.viewLocation?.addType, + dataMapperMetadata: (context, event) => event.viewLocation?.dataMapperMetadata }) } } @@ -162,7 +163,8 @@ const stateMachine = createMachine( identifier: (context, event) => event.data.identifier, position: (context, event) => event.data.position, syntaxTree: (context, event) => event.data.syntaxTree, - focusFlowDiagramView: (context, event) => event.data.focusFlowDiagramView + focusFlowDiagramView: (context, event) => event.data.focusFlowDiagramView, + dataMapperMetadata: (context, event) => event.data.dataMapperMetadata }) } } @@ -180,7 +182,8 @@ const stateMachine = createMachine( type: (context, event) => event.viewLocation?.type, isGraphql: (context, event) => event.viewLocation?.isGraphql, metadata: (context, event) => event.viewLocation?.metadata, - addType: (context, event) => event.viewLocation?.addType + addType: (context, event) => event.viewLocation?.addType, + dataMapperMetadata: (context, event) => event.viewLocation?.dataMapperMetadata }) }, VIEW_UPDATE: { @@ -193,7 +196,8 @@ const stateMachine = createMachine( serviceType: (context, event) => event.viewLocation.serviceType, type: (context, event) => event.viewLocation?.type, isGraphql: (context, event) => event.viewLocation?.isGraphql, - addType: (context, event) => event.viewLocation?.addType + addType: (context, event) => event.viewLocation?.addType, + dataMapperMetadata: (context, event) => event.viewLocation?.dataMapperMetadata }) }, FILE_EDIT: { @@ -296,7 +300,8 @@ const stateMachine = createMachine( identifier: context.identifier, type: context?.type, isGraphql: context?.isGraphql, - addType: context?.addType + addType: context?.addType, + dataMapperMetadata: context?.dataMapperMetadata } }); return resolve(); @@ -474,6 +479,16 @@ export function updateView(refreshTreeView?: boolean) { notifyCurrentWebview(); } +export function updateInlineDataMapperView(codedata?: CodeData, variableName?: string) { + let lastView: HistoryEntry = getLastHistory(); + lastView.location.dataMapperMetadata = { + codeData: codedata, + name: variableName + }; + stateService.send({ type: "VIEW_UPDATE", viewLocation: lastView.location }); + notifyCurrentWebview(); +} + function getLastHistory() { const historyStack = history?.get(); return historyStack?.[historyStack?.length - 1]; diff --git a/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/inline-data-mapper/rpc-client.ts b/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/inline-data-mapper/rpc-client.ts index 1958f578425..76b244e1c7e 100644 --- a/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/inline-data-mapper/rpc-client.ts +++ b/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/inline-data-mapper/rpc-client.ts @@ -19,6 +19,13 @@ */ import { AddArrayElementRequest, + AddClausesRequest, + ConvertToQueryRequest, + GetInlineDataMapperCodedataRequest, + GetInlineDataMapperCodedataResponse, + GetSubMappingCodedataRequest, + InitialIDMSourceRequest, + InitialIDMSourceResponse, InlineDataMapperAPI, InlineDataMapperModelRequest, InlineDataMapperModelResponse, @@ -26,9 +33,14 @@ import { InlineDataMapperSourceResponse, VisualizableFieldsRequest, VisualizableFieldsResponse, + addClauses, addNewArrayElement, + convertToQuery, + getDataMapperCodedata, getDataMapperModel, getDataMapperSource, + getInitialIDMSource, + getSubMappingCodedata, getVisualizableFields } from "@wso2/ballerina-core"; import { HOST_EXTENSION } from "vscode-messenger-common"; @@ -41,6 +53,10 @@ export class InlineDataMapperRpcClient implements InlineDataMapperAPI { this._messenger = messenger; } + getInitialIDMSource(params: InitialIDMSourceRequest): Promise { + return this._messenger.sendRequest(getInitialIDMSource, HOST_EXTENSION, params); + } + getDataMapperModel(params: InlineDataMapperModelRequest): Promise { return this._messenger.sendRequest(getDataMapperModel, HOST_EXTENSION, params); } @@ -56,4 +72,20 @@ export class InlineDataMapperRpcClient implements InlineDataMapperAPI { addNewArrayElement(params: AddArrayElementRequest): Promise { return this._messenger.sendRequest(addNewArrayElement, HOST_EXTENSION, params); } + + convertToQuery(params: ConvertToQueryRequest): Promise { + return this._messenger.sendRequest(convertToQuery, HOST_EXTENSION, params); + } + + addClauses(params: AddClausesRequest): Promise { + return this._messenger.sendRequest(addClauses, HOST_EXTENSION, params); + } + + getDataMapperCodedata(params: GetInlineDataMapperCodedataRequest): Promise { + return this._messenger.sendRequest(getDataMapperCodedata, HOST_EXTENSION, params); + } + + getSubMappingCodedata(params: GetSubMappingCodedataRequest): Promise { + return this._messenger.sendRequest(getSubMappingCodedata, HOST_EXTENSION, params); + } } diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/Form/index.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/Form/index.tsx index fbdd3bd3ede..36505c12ea3 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/Form/index.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/Form/index.tsx @@ -128,11 +128,6 @@ namespace S { } `; - export const PrimaryButton = styled(Button)` - appearance: "primary"; - display: flex; - `; - export const BodyText = styled.div<{}>` font-size: 11px; opacity: 0.5; @@ -335,7 +330,7 @@ export interface FormProps { resetUpdatedExpressionField?: () => void; mergeFormDataWithFlowNode?: (data: FormValues, targetLineRange: LineRange) => FlowNode; handleVisualizableFields?: (filePath: string, flowNode: FlowNode, position: LinePosition) => void; - visualizableFields?: string[]; + visualizableFields?: { [key: string]: string; }; recordTypeFields?: RecordTypeField[]; nestedForm?: boolean; isInferredReturnType?: boolean; @@ -404,6 +399,7 @@ export const Form = forwardRef((props: FormProps, ref) => { const markdownRef = useRef(null); const [isUserConcert, setIsUserConcert] = useState(false); + const [savingButton, setSavingButton] = useState(null); useEffect(() => { // Check if the form is a onetime usage or not. This is checked due to reset issue with nested forms in param manager @@ -579,9 +575,13 @@ export const Form = forwardRef((props: FormProps, ref) => { const hasAdvanceFields = formFields.some((field) => field.advanced && field.enabled && !field.hidden); const variableField = formFields.find((field) => field.key === "variable"); const typeField = formFields.find((field) => field.key === "type"); + const expressionField = formFields.find((field) => field.key === "expression"); const targetTypeField = formFields.find((field) => field.codedata?.kind === "PARAM_FOR_TYPE_INFER"); const dataMapperField = formFields.find((field) => field.label.includes("Data mapper")); const hasParameters = hasRequiredParameters(formFields, selectedNode) || hasOptionalParameters(formFields); + const canOpenInDataMapper = selectedNode === "VARIABLE" && + expressionField && + Object.keys(visualizableFields ?? {}).includes(expressionField.key); const contextValue: FormContext = { form: { @@ -665,6 +665,21 @@ export const Form = forwardRef((props: FormProps, ref) => { } }; + const handleOnOpenInDataMapper = () => { + setSavingButton('dataMapper'); + handleSubmit((data) => { + if (data.expression === '' && visualizableFields?.expression) { + data.expression = visualizableFields.expression; + } + return handleOnSave({ ...data, openInDataMapper: true }); + })(); + }; + + const handleOnSaveClick = () => { + setSavingButton('save'); + handleSubmit(handleOnSave)(); + }; + return ( @@ -695,9 +710,9 @@ export const Form = forwardRef((props: FormProps, ref) => { )} - {/* + {/* * Two rendering modes based on preserveOrder prop: - * + * * 1. preserveOrder = true: Render all fields in original order from formFields array * 2. preserveOrder = false: Render name and type fields at the bottom, and rest at top */} @@ -738,31 +753,30 @@ export const Form = forwardRef((props: FormProps, ref) => { {hasAdvanceFields && ( Optional Configurations - - {!showAdvancedOptions && ( - - - Expand - - )} - {showAdvancedOptions && ( - - - Collapsed + + {!showAdvancedOptions && ( + + + Expand + + )} + {showAdvancedOptions && ( + + Collapsed )} @@ -784,7 +798,6 @@ export const Form = forwardRef((props: FormProps, ref) => { openSubPanel={handleOpenSubPanel} subPanelView={subPanelView} handleOnFieldFocus={handleOnFieldFocus} - visualizableFields={visualizableFields} recordTypeFields={recordTypeFields} onIdentifierEditingStateChange={handleIdentifierEditingStateChange} /> @@ -801,7 +814,6 @@ export const Form = forwardRef((props: FormProps, ref) => { @@ -815,7 +827,6 @@ export const Form = forwardRef((props: FormProps, ref) => { openSubPanel={handleOpenSubPanel} handleOnFieldFocus={handleOnFieldFocus} handleOnTypeChange={handleOnTypeChange} - visualizableFields={visualizableFields} recordTypeFields={recordTypeFields} onIdentifierEditingStateChange={handleIdentifierEditingStateChange} /> @@ -825,7 +836,6 @@ export const Form = forwardRef((props: FormProps, ref) => { @@ -854,9 +864,26 @@ export const Form = forwardRef((props: FormProps, ref) => { {cancelText || "Cancel"}{" "} )} - - {isSaving ? {submitText || "Saving..."} : submitText || "Save"} - + {canOpenInDataMapper && + + } + )} diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/EditorFactory.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/EditorFactory.tsx index 7f07e97db5c..76b83077719 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/EditorFactory.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/EditorFactory.tsx @@ -50,7 +50,6 @@ interface FormFieldEditorProps { handleOnFieldFocus?: (key: string) => void; autoFocus?: boolean; handleOnTypeChange?: () => void; - visualizableFields?: string[]; recordTypeFields?: RecordTypeField[]; onIdentifierEditingStateChange?: (isEditing: boolean) => void; } @@ -65,7 +64,6 @@ export const EditorFactory = (props: FormFieldEditorProps) => { handleOnFieldFocus, autoFocus, handleOnTypeChange, - visualizableFields, recordTypeFields, onIdentifierEditingStateChange } = props; @@ -90,7 +88,6 @@ export const EditorFactory = (props: FormFieldEditorProps) => { subPanelView={subPanelView} handleOnFieldFocus={handleOnFieldFocus} autoFocus={autoFocus} - visualizable={visualizableFields?.includes(field.key)} recordTypeField={recordTypeFields?.find(recordField => recordField.key === field.key)} /> ); @@ -127,7 +124,6 @@ export const EditorFactory = (props: FormFieldEditorProps) => { subPanelView={subPanelView} handleOnFieldFocus={handleOnFieldFocus} autoFocus={autoFocus} - visualizable={visualizableFields?.includes(field.key)} recordTypeField={recordTypeFields?.find(recordField => recordField.key === field.key)} /> ); diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpressionEditor.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpressionEditor.tsx index 2177a2d5060..d2759099846 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpressionEditor.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpressionEditor.tsx @@ -55,7 +55,6 @@ export type ContextAwareExpressionEditorProps = { subPanelView?: SubPanelView; handleOnFieldFocus?: (key: string) => void; autoFocus?: boolean; - visualizable?: boolean; recordTypeField?: RecordTypeField; }; @@ -325,7 +324,6 @@ export const ExpressionEditor = (props: ExpressionEditorProps) => { subPanelView, targetLineRange, fileName, - visualizable, helperPaneOrigin, helperPaneHeight, recordTypeField, @@ -454,14 +452,6 @@ export const ExpressionEditor = (props: ExpressionEditorProps) => { const debouncedUpdateSubPanelData = debounce(updateSubPanelData, 300); - const codeActions = [ - visualizable && ( - - ) - ]; - const defaultValueText = field.defaultValue ? Defaults to {field.defaultValue} : null; @@ -470,7 +460,7 @@ export const ExpressionEditor = (props: ExpressionEditorProps) => { ? field.documentation : `${field.documentation}.` : ''; - + return ( {showHeader && ( @@ -562,7 +552,6 @@ export const ExpressionEditor = (props: ExpressionEditorProps) => { helperPaneWidth={recordTypeField ? 400 : undefined} growRange={growRange} sx={{ paddingInline: '0' }} - codeActions={codeActions} placeholder={placeholder} /> {error && } diff --git a/workspaces/ballerina/ballerina-visualizer/src/Hooks.tsx b/workspaces/ballerina/ballerina-visualizer/src/Hooks.tsx index b565468924d..d62cbdd4938 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/Hooks.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/Hooks.tsx @@ -19,20 +19,31 @@ import React from 'react'; import { useQuery } from '@tanstack/react-query'; import { useRpcContext } from '@wso2/ballerina-rpc-client'; -import { FlowNode, LinePosition } from '@wso2/ballerina-core'; +import { IDMViewState } from '@wso2/ballerina-core'; export const useInlineDataMapperModel = ( filePath: string, - flowNode: FlowNode, - propertyKey: string, - position: LinePosition + viewState: IDMViewState ) => { const { rpcClient } = useRpcContext(); + const viewId = viewState?.viewId; + const codedata = viewState?.codedata; + const getIDMModel = async () => { try { + const modelParams = { + filePath, + codedata, + targetField: viewId, + position: { + line: codedata.lineRange.startLine.line, + offset: codedata.lineRange.startLine.offset + } + }; const res = await rpcClient .getInlineDataMapperRpcClient() - .getDataMapperModel({ filePath, flowNode, propertyKey, position }); + .getDataMapperModel(modelParams); + console.log('>>> [Inline Data Mapper] Model:', res); return res.mappingsModel; } catch (error) { @@ -47,7 +58,7 @@ export const useInlineDataMapperModel = ( isError, refetch } = useQuery({ - queryKey: ['getIDMModel', { filePath, flowNode, position }], + queryKey: ['getIDMModel', { filePath, codedata, viewId }], queryFn: () => getIDMModel(), networkMode: 'always' }); diff --git a/workspaces/ballerina/ballerina-visualizer/src/MainPanel.tsx b/workspaces/ballerina/ballerina-visualizer/src/MainPanel.tsx index fc58d1f00ed..d8b416ca255 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/MainPanel.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/MainPanel.tsx @@ -71,6 +71,7 @@ import { ServiceClassConfig } from "./views/BI/ServiceClassEditor/ServiceClassCo import { AIAgentDesigner } from "./views/BI/AIChatAgent"; import { AIChatAgentWizard } from "./views/BI/AIChatAgent/AIChatAgentWizard"; import { BallerinaUpdateView } from "./views/BI/BallerinaUpdateView"; +import { InlineDataMapper } from "./views/InlineDataMapper"; const globalStyles = css` *, @@ -223,6 +224,15 @@ const MainPanel = () => { /> ); break; + case MACHINE_VIEW.InlineDataMapper: + setViewComponent( + + ); + break; case MACHINE_VIEW.BIDataMapperForm: setViewComponent( ), - args: + args: <> {argsDescription.map((arg) => ( @@ -1046,3 +1050,16 @@ export const isDMSupportedType = (type: VisibleTypeItem) => { return true; }; + +export function getSubPanelWidth(subPanel: SubPanel) { + if (!subPanel?.view) { + return undefined; + } + switch (subPanel.view) { + case SubPanelView.ADD_NEW_FORM: + case SubPanelView.HELPER_PANEL: + return 400; + default: + return undefined; + } +} diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/AddConnectionWizard/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/AddConnectionWizard/index.tsx index f4f0289f2bf..4f0b8dbd0de 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/AddConnectionWizard/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/AddConnectionWizard/index.tsx @@ -35,7 +35,6 @@ import ConnectionConfigView from "../ConnectionConfigView"; import { getFormProperties } from "../../../../utils/bi"; import { ExpressionFormField, FormField, FormValues, PanelContainer } from "@wso2/ballerina-side-panel"; import { Icon, Overlay, ThemeColors, Typography } from "@wso2/ui-toolkit"; -import { InlineDataMapper } from "../../../InlineDataMapper"; import { HelperView } from "../../HelperView"; import { BodyText } from "../../../styles"; import { DownloadIcon } from "../../../../components/DownloadIcon"; @@ -257,14 +256,6 @@ export function AddConnectionWizard(props: AddConnectionWizardProps) { const findSubPanelComponent = (subPanel: SubPanel) => { switch (subPanel.view) { - case SubPanelView.INLINE_DATA_MAPPER: - return ( - - ); case SubPanelView.HELPER_PANEL: return ( { switch (subPanel.view) { - case SubPanelView.INLINE_DATA_MAPPER: - return ( - - ); case SubPanelView.HELPER_PANEL: return ( void; onAddNPFunction?: () => void; onAddDataMapper?: () => void; - onSubmitForm: (updatedNode?: FlowNode, isDataMapperFormUpdate?: boolean) => void; + onSubmitForm: (updatedNode?: FlowNode, openInDataMapper?: boolean) => void; onDiscardSuggestions: () => void; onSubPanel: (subPanel: SubPanel) => void; onUpdateExpressionField: (updatedExpressionField: ExpressionFormField) => void; @@ -141,14 +140,6 @@ export function PanelManager(props: PanelManagerProps) { const findSubPanelComponent = (subPanel: SubPanel) => { switch (subPanel.view) { - case SubPanelView.INLINE_DATA_MAPPER: - return ( - - ); case SubPanelView.HELPER_PANEL: return ( {renderPanelContent()} diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/FlowDiagram/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/FlowDiagram/index.tsx index 1861a58fbc2..0ea6a38c3de 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/FlowDiagram/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/FlowDiagram/index.tsx @@ -545,18 +545,32 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { } }; - const handleOnFormSubmit = (updatedNode?: FlowNode, isDataMapperFormUpdate?: boolean) => { + const handleOnFormSubmit = (updatedNode?: FlowNode, openInDataMapper?: boolean) => { if (!updatedNode) { console.log(">>> No updated node found"); updatedNode = selectedNodeRef.current; } setShowProgressIndicator(true); + + if (openInDataMapper) { + rpcClient + .getInlineDataMapperRpcClient() + .getInitialIDMSource({ + filePath: model.fileName, + flowNode: updatedNode, + }) + .finally(() => { + setShowSidePanel(false); + setShowProgressIndicator(false); + }); + return; + } rpcClient .getBIDiagramRpcClient() .getSourceCode({ filePath: model.fileName, flowNode: updatedNode, - isFunctionNodeUpdate: isDataMapperFormUpdate, + isFunctionNodeUpdate: openInDataMapper, }) .then(async (response) => { console.log(">>> Updated source code", response); diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Forms/FormGenerator/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Forms/FormGenerator/index.tsx index a80f38f7005..ad0dd34b68b 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Forms/FormGenerator/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Forms/FormGenerator/index.tsx @@ -101,7 +101,7 @@ interface FormProps { editForm?: boolean; isGraphql?: boolean; submitText?: string; - onSubmit: (node?: FlowNode, isDataMapper?: boolean, formImports?: FormImports) => void; + onSubmit: (node?: FlowNode, openInDataMapper?: boolean, formImports?: FormImports) => void; showProgressIndicator?: boolean; subPanelView?: SubPanelView; openSubPanel?: (subPanel: SubPanel) => void; @@ -165,7 +165,7 @@ export function FormGenerator(props: FormProps) { const [fields, setFields] = useState([]); const [formImports, setFormImports] = useState({}); const [typeEditorState, setTypeEditorState] = useState({ isOpen: false, newTypeValue: "" }); - const [visualizableFields, setVisualizableFields] = useState([]); + const [visualizableFields, setVisualizableFields] = useState<{ [key: string]: string; }>(); const [recordTypeFields, setRecordTypeFields] = useState([]); /* Expression editor related state and ref variables */ @@ -288,8 +288,8 @@ export function FormGenerator(props: FormProps) { const updatedNode = mergeFormDataWithFlowNode(data, targetLineRange, dirtyFields); console.log(">>> Updated node", updatedNode); - const isDataMapperFormUpdate = data["isDataMapperFormUpdate"]; - onSubmit(updatedNode, isDataMapperFormUpdate, formImports); + const openInDataMapper = data["openInDataMapper"]; + onSubmit(updatedNode, openInDataMapper, formImports); } }; diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Forms/FormGeneratorNew/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Forms/FormGeneratorNew/index.tsx index 861fc71e36a..35834bd467b 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Forms/FormGeneratorNew/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Forms/FormGeneratorNew/index.tsx @@ -76,6 +76,7 @@ interface FormProps { submitText?: string; cancelText?: string; onBack?: () => void; + onCancel?: () => void; editForm?: boolean; isGraphqlEditor?: boolean; onSubmit: (data: FormValues, formImports?: FormImports) => void; @@ -105,6 +106,7 @@ export function FormGeneratorNew(props: FormProps) { submitText, cancelText, onBack, + onCancel, onSubmit, isSaving, isGraphqlEditor, @@ -674,7 +676,7 @@ export function FormGeneratorNew(props: FormProps) { formFields={fieldsValues} projectPath={projectPath} openRecordEditor={handleOpenTypeEditor} - onCancelForm={onBack} + onCancelForm={onBack || onCancel} submitText={submitText} cancelText={cancelText} onSubmit={handleSubmit} diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/InlineDataMapper/DataMapperView.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/InlineDataMapper/DataMapperView.tsx new file mode 100644 index 00000000000..f14b63dcf02 --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/InlineDataMapper/DataMapperView.tsx @@ -0,0 +1,290 @@ +/** + * 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 React, { useEffect, useState, useRef } from "react"; + +import { + AddArrayElementRequest, + ConvertToQueryRequest, + ExpandedDMModel, + IDMFormProps, + DMModel, + ModelState, + AddClausesRequest, + IDMViewState, + IntermediateClause +} from "@wso2/ballerina-core"; +import { ProgressIndicator } from "@wso2/ui-toolkit"; +import { useRpcContext } from "@wso2/ballerina-rpc-client"; +import { DataMapperView } from "@wso2/ballerina-inline-data-mapper"; + +import { useInlineDataMapperModel } from "../../Hooks"; +import { expandDMModel } from "./modelProcessor"; +import FormGeneratorNew from "../BI/Forms/FormGeneratorNew"; +import { InlineDataMapperProps } from "."; + +// Types for model comparison +interface ModelSignature { + inputs: string[]; + output: string; + subMappings: string[]; + types: string; +} + +export function InlineDataMapperView(props: InlineDataMapperProps) { + const { filePath, codedata, varName } = props; + + const [isFileUpdateError, setIsFileUpdateError] = useState(false); + const [modelState, setModelState] = useState({ + model: null, + hasInputsOutputsChanged: false + }); + const [viewState, setViewState] = useState({ + viewId: varName, + codedata: codedata + }); + + // Keep track of previous inputs/outputs and sub mappings for comparison + const prevSignatureRef = useRef(null); + + const { rpcClient } = useRpcContext(); + const { + model, + isFetching, + isError + } = useInlineDataMapperModel(filePath, viewState); + + useEffect(() => { + setViewState(prev => ({ + ...prev, + codedata + })); + }, [varName, codedata]); + + useEffect(() => { + if (!model) return; + + const currentSignature = JSON.stringify(getModelSignature(model)); + const prevSignature = prevSignatureRef.current; + + const hasInputsChanged = hasSignatureChanged(currentSignature, prevSignature, 'inputs'); + const hasOutputChanged = hasSignatureChanged(currentSignature, prevSignature, 'output'); + const hasSubMappingsChanged = hasSignatureChanged(currentSignature, prevSignature, 'subMappings'); + const hasTypesChanged = hasSignatureChanged(currentSignature, prevSignature, 'types'); + + // Check if it's already an ExpandedDMModel + const isExpandedModel = !('types' in model); + if (isExpandedModel) { + setModelState({ + model: model as ExpandedDMModel, + hasInputsOutputsChanged: hasInputsChanged || hasOutputChanged, + hasSubMappingsChanged: hasSubMappingsChanged + }); + prevSignatureRef.current = currentSignature; + return; + } + + // If types changed, we need to reprocess everything + if (hasTypesChanged || hasInputsChanged || hasOutputChanged || hasSubMappingsChanged) { + const expandedModel = expandDMModel(model as DMModel, { + processInputs: hasInputsChanged || hasTypesChanged, + processOutput: hasOutputChanged || hasTypesChanged, + processSubMappings: hasSubMappingsChanged || hasTypesChanged, + previousModel: modelState.model as ExpandedDMModel + }); + setModelState({ + model: expandedModel, + hasInputsOutputsChanged: hasInputsChanged || hasOutputChanged || hasTypesChanged, + hasSubMappingsChanged: hasSubMappingsChanged || hasTypesChanged + }); + } else { + setModelState(prev => ({ + model: { + ...prev.model!, + mappings: (model as DMModel).mappings + } + })); + } + + prevSignatureRef.current = currentSignature; + }, [model]); + + const onClose = () => { + rpcClient.getVisualizerRpcClient()?.goBack(); + } + + const updateExpression = async (outputId: string, expression: string, viewId: string, name: string) => { + try { + const resp = await rpcClient + .getInlineDataMapperRpcClient() + .getDataMapperSource({ + filePath, + codedata, + varName: name, + targetField: viewId, + mapping: { + output: outputId, + expression: expression + } + }); + console.log(">>> [Inline Data Mapper] getSource response:", resp); + } catch (error) { + console.error(error); + setIsFileUpdateError(true); + } + }; + + const addArrayElement = async (outputId: string, viewId: string, name: string) => { + try { + const addElementRequest: AddArrayElementRequest = { + filePath, + codedata, + varName: name, + targetField: outputId, + propertyKey: "expression" // TODO: Remove this once the API is updated + }; + const resp = await rpcClient + .getInlineDataMapperRpcClient() + .addNewArrayElement(addElementRequest); + console.log(">>> [Inline Data Mapper] addArrayElement response:", resp); + } catch (error) { + console.error(error); + setIsFileUpdateError(true); + } + }; + + const handleView = async (viewId: string, isSubMapping?: boolean) => { + if (isSubMapping) { + const resp = await rpcClient + .getInlineDataMapperRpcClient() + .getSubMappingCodedata({ + filePath, + codedata, + view: viewId + }); + console.log(">>> [Inline Data Mapper] getSubMappingCodedata response:", resp); + setViewState({viewId, codedata: resp.codedata}); + } else { + setViewState(prev => ({ + ...prev, + viewId + })); + } + }; + + const generateForm = (formProps: IDMFormProps) => { + return ( + + ) + } + + const convertToQuery = async (outputId: string, viewId: string, name: string) => { + try { + const convertToQueryRequest: ConvertToQueryRequest = { + filePath, + codedata, + varName: name, + targetField: outputId, + propertyKey: "expression" // TODO: Remove this once the API is updated + }; + const resp = await rpcClient + .getInlineDataMapperRpcClient() + .convertToQuery(convertToQueryRequest); + console.log(">>> [Inline Data Mapper] convertToQuery response:", resp); + } catch (error) { + console.error(error); + setIsFileUpdateError(true); + } + } + + const addClauses = async (clause: IntermediateClause, targetField: string, isNew: boolean, index?:number) => { + try { + const addClausesRequest: AddClausesRequest = { + filePath, + codedata: { + ...codedata, + isNew: true + }, + index, + clause, + targetField + }; + console.log(">>> [Inline Data Mapper] addClauses request:", addClausesRequest); + + const resp = await rpcClient + .getInlineDataMapperRpcClient() + .addClauses(addClausesRequest); + console.log(">>> [Inline Data Mapper] addClauses response:", resp); + } catch (error) { + console.error(error); + setIsFileUpdateError(true); + } + } + + useEffect(() => { + // Hack to hit the error boundary + if (isError) { + throw new Error("Error while fetching input/output types"); + } else if (isFileUpdateError) { + throw new Error("Error while updating file content"); + } + }, [isError]); + + return ( + <> + {isFetching && ( + + )} + {modelState.model && ( + + )} + + ); +}; + +const getModelSignature = (model: DMModel | ExpandedDMModel): ModelSignature => ({ + inputs: model.inputs.map(i => i.id), + output: model.output.id, + subMappings: model.subMappings?.map(s => s.id) || [], + types: 'types' in model ? JSON.stringify(model.types) : '' +}); + +const hasSignatureChanged = ( + current: string, + previous: string | null, + field: keyof ModelSignature +): boolean => { + if (!previous) return true; + const currentObj = JSON.parse(current); + const previousObj = JSON.parse(previous); + return JSON.stringify(currentObj[field]) !== JSON.stringify(previousObj[field]); +}; diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/InlineDataMapper/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/InlineDataMapper/index.tsx index 8687fc74439..06ce0a0bff6 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/InlineDataMapper/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/InlineDataMapper/index.tsx @@ -16,131 +16,27 @@ * under the License. */ -import React, { useEffect, useState } from "react"; +import React from "react"; -import { - AddArrayElementRequest, - FlowNode, - IDMModel, - InlineDataMapperSourceRequest, - LinePosition, - Mapping, - SubPanel, - SubPanelView -} from "@wso2/ballerina-core"; -import { DataMapperView } from "@wso2/ballerina-inline-data-mapper"; -import { ProgressIndicator } from "@wso2/ui-toolkit"; -import { useRpcContext } from "@wso2/ballerina-rpc-client"; -import { ExpressionFormField } from "@wso2/ballerina-side-panel"; +import { CodeData } from "@wso2/ballerina-core"; +import { ErrorBoundary } from "@wso2/ui-toolkit"; -import { useInlineDataMapperModel } from "../../Hooks"; +import { TopNavigationBar } from "../../components/TopNavigationBar"; +import { InlineDataMapperView } from "./DataMapperView"; -interface InlineDataMapperProps { +export interface InlineDataMapperProps { filePath: string; - flowNode: FlowNode; - propertyKey: string; - editorKey: string; - position: LinePosition; - onClosePanel: (subPanel: SubPanel) => void; - updateFormField: (data: ExpressionFormField) => void; + codedata: CodeData; + varName: string; } export function InlineDataMapper(props: InlineDataMapperProps) { - const { filePath, flowNode, propertyKey, editorKey, position, onClosePanel, updateFormField } = props; - - const [isFileUpdateError, setIsFileUpdateError] = useState(false); - const [model, setModel] = useState(null); - - const { rpcClient } = useRpcContext(); - const { - model: initialModel, - isFetching, - isError - } = useInlineDataMapperModel(filePath, flowNode, propertyKey, position); - - useEffect(() => { - if (initialModel) { - setModel(initialModel); - } - }, [initialModel]); - - const onClose = () => { - onClosePanel({ view: SubPanelView.UNDEFINED }); - } - - const updateExpression = async (mappings: Mapping[]) => { - try { - const updateSrcRequest: InlineDataMapperSourceRequest = { - filePath, - flowNode, - propertyKey, - position, - mappings - }; - const resp = await rpcClient - .getInlineDataMapperRpcClient() - .getDataMapperSource(updateSrcRequest); - console.log(">>> [Inline Data Mapper] getSource response:", resp); - const updateData: ExpressionFormField = { - value: resp.source, - key: editorKey, - cursorPosition: position - } - updateFormField(updateData); - } catch (error) { - console.error(error); - setIsFileUpdateError(true); - } - }; - - const addArrayElement = async (targetField: string) => { - try { - const addElementRequest: AddArrayElementRequest = { - filePath, - flowNode, - propertyKey, - position, - targetField - }; - const resp = await rpcClient - .getInlineDataMapperRpcClient() - .addNewArrayElement(addElementRequest); - console.log(">>> [Inline Data Mapper] addArrayElement response:", resp); - const updateData: ExpressionFormField = { - value: resp.source, - key: editorKey, - cursorPosition: position - } - updateFormField(updateData); - } catch (error) { - console.error(error); - setIsFileUpdateError(true); - } - }; - - useEffect(() => { - // Hack to hit the error boundary - if (isError) { - throw new Error("Error while fetching input/output types"); - } else if (isFileUpdateError) { - throw new Error("Error while updating file content"); - } - }, [isError]); - return ( <> - {isFetching && ( - - )} - {model && ( - - )} + + + + ); }; - diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/InlineDataMapper/modelProcessor.ts b/workspaces/ballerina/ballerina-visualizer/src/views/InlineDataMapper/modelProcessor.ts new file mode 100644 index 00000000000..daa43f2dacd --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/InlineDataMapper/modelProcessor.ts @@ -0,0 +1,202 @@ +/** + * 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 { + DMModel, + EnumType, + ExpandedDMModel, + IORoot, + IOType, + IOTypeField, + RecordType, + TypeKind, +} from "@wso2/ballerina-core"; + +interface ExpandOptions { + processInputs?: boolean; + processOutput?: boolean; + processSubMappings?: boolean; + previousModel?: ExpandedDMModel; +} + +/** + * Generates a unique field ID by combining parent ID and field name + */ +function generateFieldId(parentId: string, fieldName: string): string { + return `${parentId}.${fieldName}`; +} + +/** + * Processes a type reference and returns the appropriate IOType structure + */ +function processTypeReference( + refType: RecordType | EnumType, + fieldId: string, + model: DMModel +): Partial { + if ('fields' in refType) { + return { + fields: processTypeFields(refType, fieldId, model) + }; + } + if ('members' in refType) { + return { + members: refType.members || [] + }; + } + return {}; +} + +/** + * Processes array type fields and their members + */ +function processArray( + field: IOTypeField, + parentId: string, + member: IOTypeField, + model: DMModel +): IOType { + const fieldId = generateFieldId(parentId, `${parentId.split(".").pop()!}Item`); + const ioType: IOType = { + id: fieldId, + typeName: member.typeName!, + kind: member.kind + }; + + if (member.ref) { + const refType = model.types[member.ref]; + return { + ...ioType, + ...processTypeReference(refType, fieldId, model) + }; + } + + if (member.kind === TypeKind.Array && member.member) { + return { + ...ioType, + member: processArray(field, fieldId, member.member, model) + }; + } + + return ioType; +} + +/** + * Processes fields of a record type + */ +function processTypeFields( + type: RecordType, + parentId: string, + model: DMModel +): IOType[] { + if (!type.fields) return []; + + return type.fields.map(field => { + const fieldId = generateFieldId(parentId, field.fieldName!); + const ioType: IOType = { + id: fieldId, + variableName: field.fieldName!, + typeName: field.typeName!, + kind: field.kind + }; + + if (field.kind === TypeKind.Record && field.ref) { + const refType = model.types[field.ref]; + return { + ...ioType, + ...processTypeReference(refType, fieldId, model) + }; + } + + if (field.kind === TypeKind.Array && field.member) { + return { + ...ioType, + member: processArray(field, fieldId, field.member, model) + }; + } + + return ioType; + }); +} + +/** + * Creates a base IOType from an IORoot + */ +function createBaseIOType(root: IORoot): IOType { + return { + id: root.id, + variableName: root.fieldName!, + typeName: root.typeName, + kind: root.kind, + ...(root.category && { category: root.category }) + }; +} + +/** + * Processes an IORoot (input or output) into an IOType + */ +function processIORoot(root: IORoot, model: DMModel): IOType { + const ioType = createBaseIOType(root); + + if (root.ref) { + const refType = model.types[root.ref]; + return { + ...ioType, + ...processTypeReference(refType, root.id, model) + }; + } + + if (root.kind === TypeKind.Array && root.member) { + return { + ...ioType, + member: processArray(root, root.id, root.member, model) + }; + } + + return ioType; +} + +/** + * Expands a DMModel into an ExpandedDMModel + */ +export function expandDMModel( + model: DMModel, + options: ExpandOptions = {} +): ExpandedDMModel { + const { + processInputs = true, + processOutput = true, + processSubMappings = true, + previousModel + } = options; + + return { + inputs: processInputs + ? model.inputs.map(input => processIORoot(input, model)) + : previousModel?.inputs || [], + output: processOutput + ? processIORoot(model.output, model) + : previousModel?.output!, + subMappings: processSubMappings + ? model.subMappings?.map(subMapping => processIORoot(subMapping, model)) + : previousModel?.subMappings || [], + mappings: model.mappings, + source: "", + view: "" + }; +} diff --git a/workspaces/ballerina/inline-data-mapper/package.json b/workspaces/ballerina/inline-data-mapper/package.json index e6a823391c0..2cf391c21c6 100644 --- a/workspaces/ballerina/inline-data-mapper/package.json +++ b/workspaces/ballerina/inline-data-mapper/package.json @@ -16,7 +16,6 @@ "dependencies": { "@wso2/ballerina-core": "workspace:*", "@wso2/ballerina-rpc-client": "workspace:*", - "@wso2/syntax-tree": "workspace:*", "@wso2/ui-toolkit": "workspace:*", "@projectstorm/react-canvas-core": "^6.7.4", "@projectstorm/react-diagrams": "^6.7.4", diff --git a/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/DataMapper.tsx b/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/DataMapper.tsx index 01956f6ed96..29d2bf0c408 100644 --- a/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/DataMapper.tsx +++ b/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/DataMapper.tsx @@ -17,22 +17,41 @@ */ // tslint:disable: jsx-no-multiline-js import React, { useCallback, useEffect, useReducer, useState } from "react"; - import { css } from "@emotion/css"; +import { ExpandedDMModel } from "@wso2/ballerina-core"; import { DataMapperContext } from "../../utils/DataMapperContext/DataMapperContext"; import DataMapperDiagram from "../Diagram/Diagram"; import { DataMapperHeader } from "./Header/DataMapperHeader"; import { DataMapperNodeModel } from "../Diagram/Node/commons/DataMapperNode"; -import { NodeInitVisitor } from "../../visitors/NodeInitVisitor"; +import { IONodeInitVisitor } from "../../visitors/IONodeInitVisitor"; import { DataMapperErrorBoundary } from "./ErrorBoundary"; import { traverseNode } from "../../utils/model-utils"; import { View } from "./Views/DataMapperView"; -import { useDMCollapsedFieldsStore, useDMExpandedFieldsStore, useDMSearchStore } from "../../store/store"; +import { + useDMCollapsedFieldsStore, + useDMExpandedFieldsStore, + useDMSearchStore, + useDMSubMappingConfigPanelStore +} from "../../store/store"; import { KeyboardNavigationManager } from "../../utils/keyboard-navigation-manager"; import { DataMapperViewProps } from "../../index"; import { ErrorNodeKind } from "./Error/RenderingError"; import { IOErrorComponent } from "./Error/DataMapperError"; +import { IntermediateNodeInitVisitor } from "../../visitors/IntermediateNodeInitVisitor"; +import { + ArrayOutputNode, + InputNode, + ObjectOutputNode, + LinkConnectorNode, + QueryExprConnectorNode, + QueryOutputNode, + SubMappingNode, + EmptyInputsNode +} from "../Diagram/Node"; +import { SubMappingNodeInitVisitor } from "../../visitors/SubMappingNodeInitVisitor"; +import { SubMappingConfigForm } from "./SidePanel/SubMappingConfig/SubMappingConfigForm"; +import { ClausesPanel } from "./SidePanel/QueryClauses/ClausesPanel"; const classes = { root: css({ @@ -70,11 +89,26 @@ function viewsReducer(state: View[], action: ViewAction) { } export function InlineDataMapper(props: DataMapperViewProps) { - const { model, applyModifications, onClose, addArrayElement } = props; + const { + modelState, + name, + applyModifications, + onClose, + addArrayElement, + handleView, + convertToQuery, + generateForm, + addClauses + } = props; + const { + model, + hasInputsOutputsChanged = false, + hasSubMappingsChanged = false, + } = modelState; const initialView = [{ - label: 'Root', // TODO: Pick a better label - model: model + label: model.output.variableName, + targetField: name }]; const [views, dispatch] = useReducer(viewsReducer, initialView); @@ -82,6 +116,8 @@ export function InlineDataMapper(props: DataMapperViewProps) { const [errorKind, setErrorKind] = useState(); const [hasInternalError, setHasInternalError] = useState(false); + const { isSMConfigPanelOpen } = useDMSubMappingConfigPanelStore((state) => state.subMappingConfig); + const { resetSearchStore } = useDMSearchStore(); const addView = useCallback((view: View) => { @@ -100,28 +136,88 @@ export function InlineDataMapper(props: DataMapperViewProps) { }, [resetSearchStore]); useEffect(() => { - generateNodes(); + const lastView = views[views.length - 1]; + handleView(lastView.targetField, !!lastView?.subMappingInfo); setupKeyboardShortcuts(); return () => { KeyboardNavigationManager.getClient().resetMouseTrapInstance(); }; - }, [model, views]); + }, [views]); + + useEffect(() => { + generateNodes(model); + }, [model]); useEffect(() => { return () => { - // Cleanup on close - handleOnClose(); + // Cleanup on unmount + cleanupStores(); } }, []); - const generateNodes = () => { + const generateNodes = (model: ExpandedDMModel) => { try { - const context = new DataMapperContext(model, views, addView, applyModifications, addArrayElement); - const nodeInitVisitor = new NodeInitVisitor(context); - traverseNode(model, nodeInitVisitor); - setNodes(nodeInitVisitor.getNodes()); + const context = new DataMapperContext( + model, + views, + addView, + applyModifications, + addArrayElement, + hasInputsOutputsChanged, + convertToQuery + ); + + // Only regenerate IO nodes if inputs/outputs have changed + let ioNodes: DataMapperNodeModel[] = []; + if (hasInputsOutputsChanged || nodes.length === 0) { + const ioNodeInitVisitor = new IONodeInitVisitor(context); + traverseNode(model, ioNodeInitVisitor); + ioNodes = ioNodeInitVisitor.getNodes(); + } else { + // Reuse existing IO nodes but update their context + ioNodes = nodes + .filter(node => + node instanceof InputNode || + node instanceof ArrayOutputNode || + node instanceof ObjectOutputNode || + node instanceof QueryOutputNode + ) + .map(node => { + node.context = context; + return node; + }); + } + + // Only regenerate sub mappiing node if sub mappings have changed + const hasInputNodes = !ioNodes.some(node => node instanceof EmptyInputsNode); + let subMappingNode: DataMapperNodeModel; + if (hasInputNodes) { + if (hasSubMappingsChanged) { + const subMappingNodeInitVisitor = new SubMappingNodeInitVisitor(context); + traverseNode(model, subMappingNodeInitVisitor); + subMappingNode = subMappingNodeInitVisitor.getNode(); + } else { + // Reuse existing sub mapping node + subMappingNode = nodes.find(node => node instanceof SubMappingNode) as SubMappingNode; + } + } + + // Always regenerate intermediate nodes as they depend on mappings + const intermediateNodeInitVisitor = new IntermediateNodeInitVisitor( + context, + nodes.filter(node => node instanceof LinkConnectorNode || node instanceof QueryExprConnectorNode) + ); + traverseNode(model, intermediateNodeInitVisitor); + + // Only add subMappingNode if it is defined + setNodes([ + ...ioNodes, + ...(subMappingNode ? [subMappingNode] : []), + ...intermediateNodeInitVisitor.getNodes() + ]); } catch (error) { + console.error("Error generating nodes:", error); setHasInternalError(true); } }; @@ -132,10 +228,14 @@ export function InlineDataMapper(props: DataMapperViewProps) { mouseTrapClient.bindNewKey(['command+shift+z', 'ctrl+y'], async () => handleVersionChange('dmRedo')); }; - const handleOnClose = () => { + const cleanupStores = () => { useDMSearchStore.getState().resetSearchStore(); useDMCollapsedFieldsStore.getState().resetFields(); useDMExpandedFieldsStore.getState().resetFields(); + } + + const handleOnClose = () => { + cleanupStores(); onClose(); }; @@ -152,17 +252,33 @@ export function InlineDataMapper(props: DataMapperViewProps) {
{model && ( )} {errorKind && } {nodes.length > 0 && ( - + <> + + {isSMConfigPanelOpen && ( + + )} + )} +
) diff --git a/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/Header/AutoMapButton.tsx b/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/Header/AutoMapButton.tsx new file mode 100644 index 00000000000..a780ec8ae7b --- /dev/null +++ b/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/Header/AutoMapButton.tsx @@ -0,0 +1,46 @@ +/** + * 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 { Button, Codicon, Tooltip } from "@wso2/ui-toolkit"; + +import { useMediaQuery } from "./utils"; + +interface AutoMapButtonProps { + onClick: () => void; + disabled?: boolean; +} + +export default function AutoMapButton(props: AutoMapButtonProps) { + const { onClick, disabled } = props; + const showText = useMediaQuery('(min-width:800px)'); + + return ( + + + + ); +} diff --git a/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/Header/DataMapperHeader.tsx b/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/Header/DataMapperHeader.tsx index eb99b22bc34..50afe468601 100644 --- a/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/Header/DataMapperHeader.tsx +++ b/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/Header/DataMapperHeader.tsx @@ -18,61 +18,111 @@ // tslint:disable: jsx-no-multiline-js import React from "react"; import styled from "@emotion/styled"; -import { Button, Codicon } from "@wso2/ui-toolkit"; +import { Codicon, Icon } from "@wso2/ui-toolkit"; import HeaderSearchBox from "./HeaderSearchBox"; +import HeaderBreadcrumb from "./HeaderBreadcrumb"; +import { View } from "../Views/DataMapperView"; +import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"; +import AutoMapButton from "./AutoMapButton"; +import EditButton from "./EditButton"; export interface DataMapperHeaderProps { + views: View[]; + switchView: (index: number) => void; hasEditDisabled: boolean; onClose: () => void; } export function DataMapperHeader(props: DataMapperHeaderProps) { - const { hasEditDisabled, onClose } = props; + const { views, switchView, hasEditDisabled, onClose } = props; return ( - Data Mapper - + + + + + Data Mapper {!hasEditDisabled && ( - - - + )} - - + + + + + + {/* TODO: Implement auto map and edit */} + + + + + + ); } const HeaderContainer = styled.div` height: 56px; - width: 100%; display: flex; - flex-direction: row; - align-items: center; - padding: 0 12px; + padding: 15px; background-color: var(--vscode-editorWidget-background); + justify-content: space-between; + align-items: center; + gap: 12px; + border-bottom: 1px solid rgba(102,103,133,0.15); `; -const Title = styled.h3` +const Title = styled.h2` margin: 0; + font-size: 20px; + font-weight: 600; color: var(--vscode-foreground); - font-size: var(--vscode-font-size); `; -const RightSection = styled.div` +const RightContainer = styled.div<{ isClickable: boolean }>` display: flex; align-items: center; gap: 12px; - margin-left: auto; + pointer-events: ${({ isClickable }) => (isClickable ? 'auto' : 'none')}; + opacity: ${({ isClickable }) => (isClickable ? 1 : 0.5)}; `; -const IOFilterBar = styled.div` +const BreadCrumb = styled.div` + width: 60%; + display: flex; + align-items: baseline; + gap: 12px; + margin-left: 12px; +`; + +const FilterBar = styled.div` + flex: 3; display: flex; align-items: center; - max-width: 300px; - min-width: 200px; + justify-content: flex-end; +`; + +const IconButton = styled.div` + padding: 4px; + cursor: pointer; + border-radius: 4px; + + &:hover { + background-color: var(--vscode-toolbar-hoverBackground); + } + + & > div:first-child { + width: 24px; + height: 24px; + font-size: 24px; + } `; diff --git a/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/Header/EditButton.tsx b/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/Header/EditButton.tsx new file mode 100644 index 00000000000..4d500d376f3 --- /dev/null +++ b/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/Header/EditButton.tsx @@ -0,0 +1,47 @@ +/** + * 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 { Button, Icon, Tooltip } from "@wso2/ui-toolkit"; + +import { useMediaQuery } from "./utils"; + +interface AutoMapButtonProps { + onClick: () => void; + disabled?: boolean; +} + +export default function EditButton(props: AutoMapButtonProps) { + const { onClick, disabled } = props; + const showText = useMediaQuery('(min-width:800px)'); + + return ( + + + + ); +} diff --git a/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/Header/ExpressionBar.tsx b/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/Header/ExpressionBar.tsx index 1a9736bb5ef..08baea46129 100644 --- a/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/Header/ExpressionBar.tsx +++ b/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/Header/ExpressionBar.tsx @@ -126,7 +126,7 @@ export default function ExpressionBarWrapper(props: ExpressionBarProps) { } // Update the expression text when an input port is selected const cursorPosition = textFieldRef.current.shadowRoot.querySelector('textarea').selectionStart; - const inputAccessExpr = buildInputAccessExpr(inputPort.fieldFQN); + const inputAccessExpr = buildInputAccessExpr(inputPort.attributes.fieldFQN); const updatedText = textFieldValue.substring(0, cursorPosition) + inputAccessExpr + diff --git a/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/Header/HeaderBreadcrumb.tsx b/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/Header/HeaderBreadcrumb.tsx index dce58309906..cfa8a6ee80b 100644 --- a/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/Header/HeaderBreadcrumb.tsx +++ b/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/Header/HeaderBreadcrumb.tsx @@ -25,26 +25,22 @@ import { extractLastPartFromLabel } from './utils'; const useStyles = () => { const baseStyle = { - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis' + color: "inherit", + fontFamily: "var(--vscode-editor-font-family)", + fontSize: "13px" }; return { - baseStyle, active: css({ ...baseStyle, - cursor: "default", - lineHeight: "unset", - color: "inherit", + cursor: "default" }), link: css({ ...baseStyle, cursor: "pointer", - color: "inherit", "&:hover": { - color: "inherit" - }, + color: "var(--vscode-textLink-activeForeground)" + } }) }; }; @@ -62,19 +58,19 @@ export default function HeaderBreadcrumb(props: HeaderBreadcrumbProps) { if (views) { const focusedView = views[views.length - 1]; const otherViews = views.slice(0, -1); - let isFnDef = views.length === 1; + let isRootView = views.length === 1; let label = extractLastPartFromLabel(focusedView.label); const selectedLink = (
- {isFnDef ? label : 'Map'} + {isRootView ? label : `${label}:Query`}
); const restLinks = otherViews.length > 0 && ( otherViews.map((view, index) => { label = view.label; - isFnDef = index === 0; + isRootView = index === 0; return ( - {isFnDef ? label : 'Map'} + {isRootView ? label : `${label}:Query`} ); }) @@ -104,7 +100,6 @@ export default function HeaderBreadcrumb(props: HeaderBreadcrumbProps) { } - sx={{ width: '82%' }} > {links} {activeLink} diff --git a/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/Header/HeaderSearchBox.tsx b/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/Header/HeaderSearchBox.tsx index 4b4b1984251..19b2b887629 100644 --- a/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/Header/HeaderSearchBox.tsx +++ b/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/Header/HeaderSearchBox.tsx @@ -16,14 +16,15 @@ * under the License. */ // tslint:disable: jsx-no-multiline-js jsx-no-lambda -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useRef } from 'react'; import debounce from "lodash.debounce"; import { useDMSearchStore } from "../../../store/store"; import { getInputOutputSearchTerms } from "./utils"; -import { Codicon, SearchBox } from '@wso2/ui-toolkit'; +import { Codicon, TextField } from '@wso2/ui-toolkit'; +import { HeaderSearchBoxOptions } from './HeaderSearchBoxOptions'; export const INPUT_FIELD_FILTER_LABEL = "in:"; export const OUTPUT_FIELD_FILTER_LABEL = "out:"; @@ -40,28 +41,37 @@ export interface SearchTerm { } export default function HeaderSearchBox() { - const [searchTerm, setSearchTerm] = useState(''); - const [searchOption, setSearchOption] = useState([]); + + const [searchOptions, setSearchOptions] = useState([]); const [inputSearchTerm, setInputSearchTerm] = useState(); const [outputSearchTerm, setOutputSearchTerm] = useState(); const dmStore = useDMSearchStore.getState(); + const searchTermRef = useRef(""); + const searchInputRef = useRef(null); + + const searchOptionsData = [ + { value: INPUT_FIELD_FILTER_LABEL, label: "Filter in inputs" }, + { value: OUTPUT_FIELD_FILTER_LABEL, label: "Filter in outputs" } + ]; + const handleSearchInputChange = (text: string) => { debouncedOnChange(text); - setSearchTerm(text); + searchTermRef.current = text; }; const handleSearch = (term: string) => { const [inSearchTerm, outSearchTerm] = getInputOutputSearchTerms(term); + const hasInputFilterLabelChanged = !inputSearchTerm || (inputSearchTerm && inSearchTerm && inputSearchTerm.isLabelAvailable !== inSearchTerm.isLabelAvailable); const hasOutputFilterLabelChanged = !outputSearchTerm || (outputSearchTerm && outSearchTerm && outputSearchTerm.isLabelAvailable !== outSearchTerm.isLabelAvailable); if (hasInputFilterLabelChanged || hasOutputFilterLabelChanged) { - let modifiedSearchOptions: string[] = searchOption; + let modifiedSearchOptions: string[] = searchOptions; if (hasInputFilterLabelChanged) { - if (!searchOption.includes(INPUT_FIELD_FILTER_LABEL)) { + if (!searchOptions.includes(INPUT_FIELD_FILTER_LABEL)) { if (inSearchTerm && inSearchTerm.isLabelAvailable) { modifiedSearchOptions.push(INPUT_FIELD_FILTER_LABEL); } @@ -72,7 +82,7 @@ export default function HeaderSearchBox() { } } if (hasOutputFilterLabelChanged) { - if (!searchOption.includes(OUTPUT_FIELD_FILTER_LABEL)) { + if (!searchOptions.includes(OUTPUT_FIELD_FILTER_LABEL)) { if (outSearchTerm && outSearchTerm.isLabelAvailable) { modifiedSearchOptions.push(OUTPUT_FIELD_FILTER_LABEL); } @@ -82,34 +92,36 @@ export default function HeaderSearchBox() { } } } - setSearchOption(modifiedSearchOptions); + setSearchOptions(modifiedSearchOptions); } + setInputSearchTerm(inSearchTerm); setOutputSearchTerm(outSearchTerm); dmStore.setInputSearch(inSearchTerm.searchText.trim()); dmStore.setOutputSearch(outSearchTerm.searchText.trim()); + }; const handleOnSearchTextClear = () => { handleSearch(""); - setSearchTerm(""); + searchTermRef.current = ""; }; useEffect(() => { - const [inSearchTerm, outSearchTerm] = getInputOutputSearchTerms(searchTerm); - let modifiedSearchTerm = searchTerm; - if (searchOption.includes(INPUT_FIELD_FILTER_LABEL)) { + const [inSearchTerm, outSearchTerm] = getInputOutputSearchTerms(searchTermRef.current); + let modifiedSearchTerm = searchTermRef.current; + if (searchOptions.includes(INPUT_FIELD_FILTER_LABEL)) { if (inSearchTerm && !inSearchTerm.isLabelAvailable) { - modifiedSearchTerm += ` ${INPUT_FIELD_FILTER_LABEL}`; + modifiedSearchTerm = modifiedSearchTerm.trimEnd() + ` ${INPUT_FIELD_FILTER_LABEL}`; } } else { if (inSearchTerm && inSearchTerm.isLabelAvailable) { modifiedSearchTerm = modifiedSearchTerm.replace(`${INPUT_FIELD_FILTER_LABEL}${inSearchTerm.searchText}`, ''); } } - if (searchOption.includes(OUTPUT_FIELD_FILTER_LABEL)) { + if (searchOptions.includes(OUTPUT_FIELD_FILTER_LABEL)) { if (outSearchTerm && !outSearchTerm.isLabelAvailable) { - modifiedSearchTerm += ` ${OUTPUT_FIELD_FILTER_LABEL}`; + modifiedSearchTerm = modifiedSearchTerm.trimEnd() + ` ${OUTPUT_FIELD_FILTER_LABEL}`; } } else { if (outSearchTerm && outSearchTerm.isLabelAvailable) { @@ -117,20 +129,35 @@ export default function HeaderSearchBox() { } } handleSearch(modifiedSearchTerm); - setSearchTerm(modifiedSearchTerm); - }, [searchOption]); + searchTermRef.current = modifiedSearchTerm; + }, [searchOptions]); const debouncedOnChange = debounce((value: string) => handleSearch(value), 400); - const filterIcon = (); + const filterIcon = (); return ( - + ), + }} /> + ); } diff --git a/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/Header/HeaderSearchBoxOptions.tsx b/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/Header/HeaderSearchBoxOptions.tsx new file mode 100644 index 00000000000..dfbf7dcd9ba --- /dev/null +++ b/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/Header/HeaderSearchBoxOptions.tsx @@ -0,0 +1,95 @@ +/** + * 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 React, { useRef, useState } from 'react'; +import { CheckBox, CheckBoxGroup, ClickAwayListener, Codicon } from '@wso2/ui-toolkit'; +import styled from '@emotion/styled'; + +const DropDownContainer = styled.div({ + position: "absolute", + top: "100%", + right: "0", + zIndex: 5, + backgroundColor: "var(--vscode-sideBar-background)", + padding: "5px" +}); + +const SearchBoxOptionsContainer=styled.div({ + display: "flex", flexDirection: "row" +}); + +interface HeaderSearchBoxOptionsProps { + searchTerm: string; + searchInputRef: React.RefObject; + searchOptions: string[]; + searchOptionsData: { value: string, label: string }[]; + setSearchOptions: React.Dispatch>; + handleOnSearchTextClear: () => void; +} + +export function HeaderSearchBoxOptions(props: HeaderSearchBoxOptionsProps) { + const { searchTerm, searchInputRef, searchOptions, setSearchOptions, handleOnSearchTextClear, searchOptionsData } = props; + + const [showSearchOptions, setShowSearchOptions] = useState(false); + const showSearchOptionsRef = useRef(null); + + const handleSearchOptionsChange = (checked: boolean, value: string) => { + if (checked) { + if (searchOptions.indexOf(value) === -1) { + setSearchOptions([value, ...searchOptions]); + } + } else { + setSearchOptions(searchOptions.filter(option => option !== value)); + } + searchInputRef.current.shadowRoot.querySelector('input').focus(); + }; + + return ( + + {searchTerm && ( + + )} + +
+
+ setShowSearchOptions(!showSearchOptions)} /> +
+ { setShowSearchOptions(false); }} anchorEl={showSearchOptionsRef.current}> + {showSearchOptions && ( + + + {searchOptionsData.map((item) => ( + -1} + label={item.label} + onChange={(checked) => { + handleSearchOptionsChange(checked, item.value); + }} + value={item.value} + /> + ))} + + + )} + +
+
+ ); +}; + +export default HeaderSearchBoxOptions; diff --git a/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/Header/utils.tsx b/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/Header/utils.tsx index d700e3035d8..1e7a953402e 100644 --- a/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/Header/utils.tsx +++ b/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/Header/utils.tsx @@ -1,22 +1,17 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. +/* + * Copyright (c) 2024, WSO2 LLC. (http://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. + * This software is the property of WSO2 LLC. and its suppliers, if any. + * Dissemination of any information or reproduction of any material contained + * herein is strictly forbidden, unless permitted by WSO2 in accordance with + * the WSO2 Commercial License available at http://wso2.com/licenses. + * For specific language governing the permissions and limitations under + * this license, please see the license as well as any agreement you’ve + * entered into with WSO2 governing the purchase of this software and any + * associated services. */ +import { useEffect, useState } from "react"; import { INPUT_FIELD_FILTER_LABEL, OUTPUT_FIELD_FILTER_LABEL, SearchTerm, SearchType } from "./HeaderSearchBox"; -import { View } from "../Views/DataMapperView"; export function getInputOutputSearchTerms(searchTerm: string): [SearchTerm, SearchTerm] { const inputFilter = INPUT_FIELD_FILTER_LABEL; @@ -81,3 +76,23 @@ export function extractLastPartFromLabel(targetLabel: string): string | null { return targetLabel; } + +export function useMediaQuery(query: string): boolean { + const [matches, setMatches] = useState(false); + + useEffect(() => { + const media = window.matchMedia(query); + if (media.matches !== matches) { + setMatches(media.matches); + } + + const listener = () => { + setMatches(media.matches); + }; + + media.addEventListener("change", listener); + return () => media.removeEventListener("change", listener); + }, [matches, query]); + + return matches; +} diff --git a/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/SidePanel/QueryClauses/ClauseEditor.tsx b/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/SidePanel/QueryClauses/ClauseEditor.tsx new file mode 100644 index 00000000000..73f1c23edfb --- /dev/null +++ b/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/SidePanel/QueryClauses/ClauseEditor.tsx @@ -0,0 +1,147 @@ +/** + * 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 React from "react"; +import { EditorContainer } from "./styles"; +import { Divider, Dropdown, OptionProps, Typography } from "@wso2/ui-toolkit"; +import { IDMFormProps, IDMFormField, IDMFormFieldValues, IntermediateClauseType, IntermediateClause, IntermediateClauseProps } from "@wso2/ballerina-core"; + +export interface ClauseEditorProps { + clause?: IntermediateClause; + onSubmitText?: string; + isSaving: boolean; + onSubmit: (clause: IntermediateClause) => void; + onCancel: () => void; + generateForm: (formProps: IDMFormProps) => JSX.Element; +} + +export function ClauseEditor(props: ClauseEditorProps) { + const { clause, onSubmitText, isSaving, onSubmit, onCancel, generateForm } = props; + const { type: _clauseType, properties: clauseProps } = clause ?? {}; + + const [clauseType, setClauseType] = React.useState(_clauseType ?? IntermediateClauseType.WHERE); + const clauseTypeItems: OptionProps[] = [ + { content: "condition", value: IntermediateClauseType.WHERE }, + { content: "local variable", value: IntermediateClauseType.LET }, + { content: "sort by", value: IntermediateClauseType.ORDER_BY }, + { content: "limit", value: IntermediateClauseType.LIMIT }, + { content: "from", value: IntermediateClauseType.FROM } + ] + + const nameField: IDMFormField = { + key: "name", + label: "Name", + type: "IDENTIFIER", + optional: false, + editable: true, + documentation: "Enter the name of the tool.", + value: clauseProps?.name ?? "", + valueTypeConstraint: "Global", + enabled: true, + } + + const typeField: IDMFormField = { + key: "type", + label: "Type", + type: "TYPE", + optional: false, + editable: true, + documentation: "Enter the type of the clause.", + value: clauseProps?.type ?? "", + valueTypeConstraint: "Global", + enabled: true, + } + + const expressionField: IDMFormField = { + key: "expression", + label: "Expression", + type: "EXPRESSION", + optional: false, + editable: true, + documentation: "Enter the expression of the clause.", + value: clauseProps?.expression ?? "", + valueTypeConstraint: "Global", + enabled: true, + } + + const orderField: IDMFormField = { + key: "order", + label: "Order", + type: "ENUM", + optional: false, + editable: true, + documentation: "Enter the order.", + value: clauseProps?.order ?? "", + valueTypeConstraint: "Global", + enabled: true, + items: ["ascending", "descending"] + } + + const handleSubmit = (data: IDMFormFieldValues) => { + onSubmit({ + type: clauseType as IntermediateClauseType, + properties: data as IntermediateClauseProps + }); + } + + // function with select case to gen fields based on clause type + const generateFields = () => { + switch (clauseType) { + case IntermediateClauseType.LET: + case IntermediateClauseType.FROM: + return [nameField, typeField, expressionField]; + case IntermediateClauseType.ORDER_BY: + return [expressionField, orderField]; + default: + return [expressionField]; + } + } + + const formProps: IDMFormProps = { + targetLineRange:{ startLine: { line: 0, offset: 0 }, endLine: { line: 0, offset: 0 } }, + fields: generateFields(), + submitText: onSubmitText || "Add", + cancelText: "Cancel", + nestedForm: true, + onSubmit: handleSubmit, + onCancel, + isSaving, + helperPaneSide: 'left' + } + + return ( + + Clause Configuration + + + + + {generateForm(formProps)} + + + ); +} diff --git a/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/SidePanel/QueryClauses/ClauseItem.tsx b/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/SidePanel/QueryClauses/ClauseItem.tsx new file mode 100644 index 00000000000..bb45b8ba6fb --- /dev/null +++ b/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/SidePanel/QueryClauses/ClauseItem.tsx @@ -0,0 +1,140 @@ +/** + * 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 React from "react"; +import { + HeaderLabel, + ContentWrapper, + IconTextWrapper, + TypeWrapper, + IconWrapper, + ValueTextWrapper, + ActionWrapper, + ActionIconWrapper, + EditIconWrapper, + DeleteIconWrapper, + AddIconContainer, + AddIcon, + ProgressRingWrapper +} from "./styles"; +import { Button, Codicon, ProgressRing } from "@wso2/ui-toolkit"; +import { ClauseEditor } from "./ClauseEditor"; +import { IDMFormProps, IntermediateClause, IntermediateClauseType } from "@wso2/ballerina-core"; +import { set } from "lodash"; + +export interface ClauseItemProps { + index: number; + clause: IntermediateClause; + isSaving: boolean; + isAdding: boolean; + isEditing: boolean; + isDeleting: boolean; + setAdding: (index: number) => void; + setEditing: (index: number) => void; + onAdd: (clause: IntermediateClause, index?: number) => void; + onEdit: (clause: IntermediateClause, index: number) => void; + onDelete: (index: number) => void; + generateForm: (formProps: IDMFormProps) => JSX.Element; +} + +export function ClauseItem(props: ClauseItemProps) { + const { index, clause, isSaving, isAdding, isEditing, isDeleting, setAdding, setEditing, onDelete, onEdit, onAdd, generateForm } = props; + const { type: clauseType, properties: clauseProps } = clause; + + + const onHandleEdit = (clause: IntermediateClause) => { + onEdit(clause, index); + } + + const onHandleDelete = () => { + onDelete(index); + } + + const onHandleAdd = (clause: IntermediateClause) => { + onAdd(clause, index); + } + + const label = + clauseType === IntermediateClauseType.LET ? `${clauseProps.type} ${clauseProps.name} = ${clauseProps.expression}` : + clauseType === IntermediateClauseType.ORDER_BY ? `${clauseProps.expression} ${clauseProps.order}` : + clauseProps.expression; + + return ( + <> + {isAdding ? ( + setAdding(-1)} + onSubmit={onHandleAdd} + generateForm={generateForm} /> + ) : ( + setAdding(index)} /> + )} + + + setEditing(index)}> + + + {clauseType} + + {label} + + + + + setEditing(index)} /> + + {isDeleting ? ( + + + + ) : ( + + + + )} + + + + + {isEditing && ( + setEditing(-1)} + generateForm={generateForm} + /> + )} + + ); +} + +export function AddButton(props: { onClick: () => void }) { + return ( + + + + ) +} diff --git a/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/SidePanel/QueryClauses/ClausesPanel.tsx b/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/SidePanel/QueryClauses/ClausesPanel.tsx new file mode 100644 index 00000000000..625f12d03fa --- /dev/null +++ b/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/SidePanel/QueryClauses/ClausesPanel.tsx @@ -0,0 +1,141 @@ +/** + * 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 React from "react"; + +import { Button, Codicon, SidePanel, SidePanelBody, SidePanelTitleContainer, ThemeColors } from "@wso2/ui-toolkit"; +import { useDMQueryClausesPanelStore } from "../../../../store/store"; +import { AddButton, ClauseItem } from "./ClauseItem"; +import { ClauseEditor } from "./ClauseEditor"; +import { ClauseItemListContainer } from "./styles"; +import { IDMFormProps, IntermediateClause, Query } from "@wso2/ballerina-core"; + +export interface ClausesPanelProps { + query: Query; + targetField: string; + addClauses: (clause: IntermediateClause, targetField: string, isNew: boolean, index?:number) => Promise; + generateForm: (formProps: IDMFormProps) => JSX.Element; +} + +export function ClausesPanel(props: ClausesPanelProps) { + const { isQueryClausesPanelOpen, setIsQueryClausesPanelOpen } = useDMQueryClausesPanelStore(); + const { query, targetField, addClauses, generateForm } = props; + + const [adding, setAdding] = React.useState(-1); + const [editing, setEditing] = React.useState(-1); + const [deleting, setDeleting] = React.useState(-1); + const [saving, setSaving] = React.useState(-1); + const intermediateClauses = query?.intermediateClauses || []; + const clauses = intermediateClauses.map(clause => ({ + type: clause.type, + properties: { + expression: (clause as any).clause, + } + })); + + // const _addClauses = (clause: IntermediateClause, targetField: string, isNew?: boolean, index?:number): Promise => { + // return new Promise((resolve) => { + // console.log(`Simulating addClauses for targetField: ${targetField}`); + // setTimeout(() => { + // console.log(`addClauses completed for targetField: ${targetField}`); + // resolve(); + // }, 1000); // Simulate a 1-second delay + // }); + // }; + + const setClauses = async (clause: IntermediateClause, isNew: boolean, index?: number) => { + setSaving(index); + // await addClauses(clause, targetField, isNew, index); + await addClauses(clause, targetField, true, undefined); + setSaving(-1); + } + + const onAdd = async (clause: IntermediateClause, index?: number) => { + await setClauses(clause, true, index); + setAdding(-1); + } + + const onDelete = async (index: number) => { + setDeleting(index); + await setClauses( undefined, false, index); + setDeleting(-1); + } + + const onEdit = async (clause: IntermediateClause, index: number) => { + await setClauses(clause, false, index); + setEditing(-1); + } + + return ( + + + Query Filters + + + + Add filters or local variables to the query expression + + + {clauses.map((clause, index) => ( + + ))} + + + {(adding === clauses.length) ? ( + setAdding(-1)} + onSubmit={onAdd} + generateForm={generateForm} + /> + ) : ( + setAdding(clauses.length)} /> + )} + + + ); +} diff --git a/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/SidePanel/QueryClauses/styles.tsx b/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/SidePanel/QueryClauses/styles.tsx new file mode 100644 index 00000000000..d5e54db16b6 --- /dev/null +++ b/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/SidePanel/QueryClauses/styles.tsx @@ -0,0 +1,195 @@ +/** + * 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: no-explicit-any +import { css } from "@emotion/css"; + +import styled from "@emotion/styled"; +import { Icon } from "@wso2/ui-toolkit"; + +export interface ContainerProps { + readonly?: boolean; +} + +export const ClauseItemListContainer = styled.div` + margin-top: 10px; +`; + +export const EditorContainer = styled.div` + display: flex; + margin: 10px 0; + flex-direction: column; + border-radius: 5px; + padding: 10px; + border: 1px solid var(--vscode-dropdown-border); + gap: 15px; +`; + +export const EditorContent = styled.div` + display: flex; + flex-direction: column; + justify-content: space-between; + padding-left: 16px; + padding-right: 16px; + gap: 10px; +`; + + +export const ActionWrapper = styled.div` + display: flex; + flex-direction: row; +`; + +export const EditIconWrapper = styled.div` + cursor: pointer; + height: 14px; + width: 14px; + margin-top: 16px; + margin-bottom: 13px; + margin-left: 10px; + color: var(--vscode-statusBarItem-remoteBackground); +`; + +export const ProgressRingWrapper = styled.div` + display: flex; + align-items: center; + justify-content: center; + height: 32px; + width: 32px; + margin-left: 10px; +`; + +export const DeleteIconWrapper = styled.div` + cursor: pointer; + height: 14px; + width: 14px; + margin-top: 16px; + margin-bottom: 13px; + margin-left: 10px; + color: var(--vscode-notificationsErrorIcon-foreground); +`; + +export const AddIconContainer = styled.div` + display: flex; + align-items: center; + justify-content: center; + padding: 8px; +`; + +export const AddIcon = styled(Icon)` + cursor: pointer; + font-size: 20px; + color: var(--vscode-inputOption-activeForeground); + transition: all 0.2s; + &:hover { + color: var(--vscode-textLink-foreground); + } +`; + +export const IconWrapper = styled.div` + cursor: pointer; + height: 14px; + width: 14px; + margin-top: 16px; + margin-bottom: 13px; + margin-left: 10px; + margin-right: 10px; +`; + +export const ContentWrapper = styled.div` + display: flex; + justify-content: flex-start; + align-items: center; + flex-direction: row; + width: ${(props: ContainerProps) => `${props.readonly ? "100%" : "calc(100% - 60px)"}`}; + cursor: ${(props: ContainerProps) => `${props.readonly ? "default" : "pointer"}`}; + height: 100%; + color: var(--vscode-editor-foreground); + &:hover, &.active { + ${(props: ContainerProps) => `${props.readonly ? "" : "background: var(--vscode-welcomePage-tileHoverBackground)"}`}; + }; +`; + +export const KeyTextWrapper = styled.div` + display: flex; + justify-content: flex-start; + align-items: center; + flex-direction: row; + width: 150px; + background-color: var(--vscode-inputValidation-infoBackground); + height: 100%; +`; + +export const Key = styled.div` + cursor: pointer; + margin-left: 10px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +`; + +export const IconTextWrapper = styled.div` + display: flex; + justify-content: flex-start; + align-items: center; + flex-direction: row; + width: 100px; + background-color: var(--vscode-inputValidation-infoBackground); + height: 100%; +`; + +export const ValueTextWrapper = styled.div` + display: flex; + justify-content: flex-start; + align-items: center; + flex-direction: row; + padding: 0 10px; + height: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +`; + +export const OptionLabel = styled.div` + font-size: 12px; + line-height: 14px; + margin-left: 5px; +`; + +export const HeaderLabel = styled.div` + display: flex; + background: var(--vscode-editor-background); + border: 1px solid var(--vscode-dropdown-border); + display: flex; + width: 100%; + height: 32px; + align-items: center; +`; + +export const ActionIconWrapper = styled.div` + display: flex; + align-items: center; + cursor: pointer; + height: 14px; + width: 14px; +`; + +export const TypeWrapper = styled.div` + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +`; diff --git a/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/SidePanel/SubMappingConfig/SubMappingConfigForm.tsx b/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/SidePanel/SubMappingConfig/SubMappingConfigForm.tsx new file mode 100644 index 00000000000..8aebbc8297b --- /dev/null +++ b/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/SidePanel/SubMappingConfig/SubMappingConfigForm.tsx @@ -0,0 +1,241 @@ +/** + * 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 React, { useEffect } from "react"; +import { + AutoComplete, + Button, + Codicon, + LinkButton, + SidePanel, + SidePanelBody, + SidePanelTitleContainer, + TextField, + ThemeColors +} from "@wso2/ui-toolkit"; +import styled from "@emotion/styled"; +import { VSCodeCheckbox } from '@vscode/webview-ui-toolkit/react'; +import { Controller, useForm } from 'react-hook-form'; + +import { useDMSubMappingConfigPanelStore, SubMappingConfigFormData } from "../../../../store/store"; +import { View } from "../../Views/DataMapperView"; + +const Field = styled.div` + display: flex; + flex-direction: column; + margin-bottom: 12px; +`; + +const ALLOWED_TYPES = ['string', 'number', 'boolean', 'object']; +const ADD_NEW_SUB_MAPPING_HEADER = "Add New Sub Mapping"; +const EDIT_SUB_MAPPING_HEADER = "Edit Sub Mapping"; + +export type SubMappingConfigFormProps = { + views: View[]; + updateView: (updatedView: View) => void; + applyModifications: (outputId: string, expression: string, viewId: string, name: string) => Promise +}; + +export function SubMappingConfigForm(props: SubMappingConfigFormProps) { + const { views, updateView, applyModifications } = props; + const lastView = views && views[views.length - 1]; + + const allowedTypes = [...ALLOWED_TYPES]; + + const { + subMappingConfig: { isSMConfigPanelOpen, nextSubMappingIndex, suggestedNextSubMappingName }, + resetSubMappingConfig, + subMappingConfigFormData, + setSubMappingConfigFormData + } = useDMSubMappingConfigPanelStore(state => ({ + subMappingConfig: state.subMappingConfig, + resetSubMappingConfig: state.resetSubMappingConfig, + subMappingConfigFormData: state.subMappingConfigFormData, + setSubMappingConfigFormData: state.setSubMappingConfigFormData + }) + ); + + let defaultValues: { mappingName: string; mappingType: string | null; isArray: boolean }; + if (subMappingConfigFormData) { + defaultValues = { + mappingName: subMappingConfigFormData.mappingName, + mappingType: subMappingConfigFormData.mappingType, + isArray: subMappingConfigFormData.isArray + } + } else { + defaultValues = { + mappingName: suggestedNextSubMappingName, + mappingType: null, + isArray: false + } + } + + const { control, handleSubmit, setValue, watch, reset, getValues } = useForm({ defaultValues }); + + const isEdit = nextSubMappingIndex === -1 && !suggestedNextSubMappingName; + + const getIsArray = (mappingType: string) => { + return mappingType.includes('[]'); + }; + + const getBaseType = (mappingType: string) => { + return mappingType.replaceAll('[]', ''); + }; + + useEffect(() => { + if (isEdit) { + const { mappingName, mappingType } = lastView.subMappingInfo; + setValue('mappingName', mappingName); + setValue('mappingType', getBaseType(mappingType)); + setValue('isArray', getIsArray(mappingType)); + } else { + setValue('mappingName', defaultValues.mappingName); + setValue('mappingType', defaultValues.mappingType); + setValue('isArray', defaultValues.isArray); + } + }, [isEdit, defaultValues.mappingName, defaultValues.mappingType, defaultValues.isArray, setValue]); + + const onAdd = async (data: SubMappingConfigFormData) => { + // TODO: Implement onAdd + }; + + + const onEdit = async (data: SubMappingConfigFormData) => { + // TODO: Implement onEdit + resetSubMappingConfig(); + reset(); + }; + + const onClose = () => { + resetSubMappingConfig(); + }; + + const openImportCustomTypeForm = () => { + setSubMappingConfigFormData(getValues()); + } + + return ( + + + {isEdit ? EDIT_SUB_MAPPING_HEADER : ADD_NEW_SUB_MAPPING_HEADER} + + + + + ( + + )} + /> + + + ( + <> + { field.onChange(e); }} + borderBox + /> + + )} + /> + + + +

Add new type

+
+ +
+ + ( + field.onChange(e.target.checked)} + onBlur={field.onBlur} + name={field.name} + ref={field.ref} + > + Is Array + + )} + /> + + {!isEdit && ( +
+ +
+ )} + {isEdit && ( +
+ +
+ )} +
+
+ ); +} diff --git a/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/Views/DataMapperView.ts b/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/Views/DataMapperView.ts index 40a862581d5..f68d83e340b 100644 --- a/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/Views/DataMapperView.ts +++ b/workspaces/ballerina/inline-data-mapper/src/components/DataMapper/Views/DataMapperView.ts @@ -15,10 +15,16 @@ * specific language governing permissions and limitations * under the License. */ - -import { IDMModel } from "@wso2/ballerina-core"; - export interface View { label: string; - model: IDMModel; + targetField?: string; + subMappingInfo?: SubMappingInfo; +} + +export interface SubMappingInfo { + index: number; + mappingName: string; + mappingType: string; + mapFnIndex?: number; + focusedOnSubMappingRoot?: boolean; } diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Actions/IONodesScrollCanvasAction.ts b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Actions/IONodesScrollCanvasAction.ts index 46a60fb99bb..439ef584c76 100644 --- a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Actions/IONodesScrollCanvasAction.ts +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Actions/IONodesScrollCanvasAction.ts @@ -27,6 +27,7 @@ import { isOutputNode } from "./utils"; import { IO_NODE_DEFAULT_WIDTH, VISUALIZER_PADDING, defaultModelOptions } from "../utils/constants"; +import { LinkConnectorNode, QueryExprConnectorNode } from "../Node"; export interface PanAndZoomCanvasActionOptions { inverseZoom?: boolean; @@ -154,9 +155,9 @@ function repositionIntermediateNodes(outputNode: NodeModel) { if (link instanceof DataMapperLinkModel) { const sourceNode = link.getSourcePort().getNode(); const targetPortPosition = link.getTargetPort().getPosition(); - // if (sourceNode instanceof LinkConnectorNode || sourceNode instanceof ArrayFnConnectorNode) { - // sourceNode.setPosition(sourceNode.getX(), targetPortPosition.y - 4.5); - // } + if (sourceNode instanceof LinkConnectorNode || sourceNode instanceof QueryExprConnectorNode) { + sourceNode.setPosition(sourceNode.getX(), targetPortPosition.y - 4.5); + } } } } diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Actions/utils.ts b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Actions/utils.ts index e13c5f7b543..aa47689df42 100644 --- a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Actions/utils.ts +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Actions/utils.ts @@ -20,22 +20,32 @@ import { BaseModel } from "@projectstorm/react-canvas-core"; import { ObjectOutputNode, InputNode, - ArrayOutputNode + ArrayOutputNode, + PrimitiveOutputNode, + QueryExprConnectorNode, + LinkConnectorNode, + QueryOutputNode, + SubMappingNode } from "../Node"; import { IO_NODE_DEFAULT_WIDTH } from "../utils/constants"; -import { DataMapperNodeModel } from "../Node/commons/DataMapperNode"; import { DataMapperLinkModel } from "../Link"; export const INPUT_NODES = [ - InputNode + InputNode, + SubMappingNode ]; export const OUTPUT_NODES = [ ObjectOutputNode, - ArrayOutputNode + ArrayOutputNode, + PrimitiveOutputNode, + QueryOutputNode ]; -export const INTERMEDIATE_NODES: typeof DataMapperNodeModel[] = []; +export const INTERMEDIATE_NODES = [ + LinkConnectorNode, + QueryExprConnectorNode +]; export const MIN_VISIBLE_HEIGHT = 68; export const INPUT_NODE_DEFAULT_RIGHT_X = IO_NODE_DEFAULT_WIDTH; diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Canvas/DataMapperCanvasContainerWidget.tsx b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Canvas/DataMapperCanvasContainerWidget.tsx index 3420146a0a8..845265b6abc 100644 --- a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Canvas/DataMapperCanvasContainerWidget.tsx +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Canvas/DataMapperCanvasContainerWidget.tsx @@ -29,9 +29,9 @@ type ContainerProps = { export const Container = styled.div` // should take up full height minus the height of the header height: calc(100% - 50px); - background-image: radial-gradient(circle at 0.5px 0.5px, var(--vscode-textBlockQuote-border) 1px, transparent 0); - background-size: 8px 8px; - background-color: var(--vscode-input-background); + background-image: radial-gradient(var(--vscode-editor-inactiveSelectionBackground) 10%, transparent 0px); + background-size: 16px 16px; + background-color: var(--vscode-editor-background); display: ${(props: { hidden: any; }) => (props.hidden ? 'none' : 'flex')}; font-weight: 400; > * { diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Diagram.tsx b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Diagram.tsx index 3e1fc87fe2d..35d19605152 100644 --- a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Diagram.tsx +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Diagram.tsx @@ -16,7 +16,7 @@ * under the License. */ // tslint:disable: jsx-no-multiline-js jsx-no-lambda -import React, { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; import { SelectionBoxLayerFactory } from "@projectstorm/react-canvas-core"; import { @@ -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 } from './Node'; +import { LinkConnectorNode, QueryExprConnectorNode } from './Node'; import { OverlayLayerFactory } from './OverlayLayer/OverlayLayerFactory'; import { OverriddenLinkLayerFactory } from './OverriddenLinkLayer/LinkLayerFactory'; import { useDiagramModel, useRepositionedNodes } from '../Hooks'; @@ -46,6 +46,7 @@ import { IONodesScrollCanvasAction } from './Actions/IONodesScrollCanvasAction'; import { useDMExpressionBarStore, useDMSearchStore } from '../../store/store'; import { isOutputNode } from './Actions/utils'; import { InputOutputPortModel } from './Port'; +import { calculateZoomLevel } from './utils/diagram-utils'; import * as Nodes from "./Node"; import * as Ports from "./Port"; import * as Labels from "./Label"; @@ -79,9 +80,13 @@ function initDiagramEngine() { engine.getLayerFactories().registerFactory(new OverlayLayerFactory()); engine.getNodeFactories().registerFactory(new Nodes.InputNodeFactory()); + engine.getNodeFactories().registerFactory(new Nodes.SubMappingNodeFactory()); engine.getNodeFactories().registerFactory(new Nodes.ObjectOutputNodeFactory()); engine.getNodeFactories().registerFactory(new Nodes.ArrayOutputNodeFactory()); + engine.getNodeFactories().registerFactory(new Nodes.PrimitiveOutputNodeFactory()); + engine.getNodeFactories().registerFactory(new Nodes.QueryOutputNodeFactory()); engine.getNodeFactories().registerFactory(new Nodes.LinkConnectorNodeFactory()); + engine.getNodeFactories().registerFactory(new Nodes.QueryExprConnectorNodeFactory()); engine.getNodeFactories().registerFactory(new Nodes.DataImportNodeFactory()); engine.getNodeFactories().registerFactory(new Nodes.EmptyInputsNodeFactory()); @@ -117,7 +122,7 @@ function DataMapperDiagram(props: DataMapperDiagramProps): React.ReactElement { const { inputSearch, outputSearch } = useDMSearchStore.getState(); - const zoomLevel = defaultModelOptions.zoom; + const zoomLevel = calculateZoomLevel(screenWidth); const repositionedNodes = useRepositionedNodes(nodes, zoomLevel, diagramModel); const { updatedModel, isFetching } = useDiagramModel(repositionedNodes, diagramModel, onError, zoomLevel); @@ -149,14 +154,14 @@ 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 LinkConnectorNode || node instanceof QueryExprConnectorNode ); - nodesToUpdate.forEach((node: LinkConnectorNode) => { + nodesToUpdate.forEach((node: LinkConnectorNode | QueryExprConnectorNode) => { node.initLinks(); const targetPortPosition = node.targetPort?.getPosition(); if (targetPortPosition) { - node.setPosition(targetPortPosition.x - 150, targetPortPosition.y - 6.5); + node.setPosition(targetPortPosition.x - 155, targetPortPosition.y - 6.5); } }); diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Label/ExpressionLabelFactory.tsx b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Label/ExpressionLabelFactory.tsx index a7fd04c99c4..e002d562acf 100644 --- a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Label/ExpressionLabelFactory.tsx +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Label/ExpressionLabelFactory.tsx @@ -22,6 +22,7 @@ import { DiagramEngine } from '@projectstorm/react-diagrams'; import { ExpressionLabelModel } from './ExpressionLabelModel'; import { ExpressionLabelWidget } from './ExpressionLabelWidget'; +import { QueryExprLabelWidget } from './QueryExprLabelWidget'; export class ExpressionLabelFactory extends AbstractReactFactory { constructor() { @@ -33,6 +34,9 @@ export class ExpressionLabelFactory extends AbstractReactFactory): JSX.Element { + if (event.model.isQuery) { + return ; + } return ; } } diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Label/ExpressionLabelModel.ts b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Label/ExpressionLabelModel.ts index 772d4191482..5f16ef35df2 100644 --- a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Label/ExpressionLabelModel.ts +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Label/ExpressionLabelModel.ts @@ -28,12 +28,15 @@ export interface ExpressionLabelOptions extends BaseModelOptions { link?: DataMapperLinkModel; field?: Node; editorLabel?: string; + isQuery?: boolean; deleteLink?: () => void; } export class ExpressionLabelModel extends LabelModel { + context?: IDataMapperContext; link?: DataMapperLinkModel; value?: string; + isQuery?: boolean; deleteLink?: () => void; constructor(options: ExpressionLabelOptions = {}) { @@ -41,8 +44,10 @@ export class ExpressionLabelModel extends LabelModel { ...options, type: 'expression-label' }); + this.context = options.context; this.link = options.link; this.value = options.value || ''; + this.isQuery = options.isQuery; this.updateSource = this.updateSource.bind(this); this.deleteLink = options.deleteLink; } diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Label/ExpressionLabelWidget.tsx b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Label/ExpressionLabelWidget.tsx index 2799f956529..6abac97f4c7 100644 --- a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Label/ExpressionLabelWidget.tsx +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Label/ExpressionLabelWidget.tsx @@ -28,7 +28,7 @@ import { ExpressionLabelModel } from './ExpressionLabelModel'; import { isSourcePortArray, isTargetPortArray } from '../utils/link-utils'; import { DataMapperLinkModel } from '../Link'; import { CodeActionWidget } from '../CodeAction/CodeAction'; -import { set } from 'lodash'; +import { InputOutputPortModel } from '../Port'; export interface ExpressionLabelWidgetProps { model: ExpressionLabelModel; @@ -118,7 +118,9 @@ export function ExpressionLabelWidget(props: ExpressionLabelWidgetProps) { const [deleteInProgress, setDeleteInProgress] = useState(false); const classes = useStyles(); - const { link, value, deleteLink } = props.model; + const { link, value, deleteLink, context } = props.model; + const { convertToQuery } = context; + const targetPort = link?.getTargetPort() as InputOutputPortModel; const diagnostic = link && link.hasError() ? link.diagnostics[0] || link.diagnostics[0] : null; const handleLinkStatus = (isSelected: boolean) => { @@ -197,19 +199,17 @@ export function ExpressionLabelWidget(props: ExpressionLabelWidgetProps) { ), ]; - const onClickMapViaArrayFn = async () => { - // TODO: Implement + const onClickMapWithQuery = async () => { + const varName = context.views[0].targetField; + const viewId = context.views[context.views.length - 1].targetField; + await convertToQuery(`${targetPort.attributes.value.output}`, `${viewId}`, `${varName}`); }; - - const applyArrayFunction = async (linkModel: DataMapperLinkModel, targetType: IDMType) => { - // TODO: Implement - }; - + const codeActions = []; if (arrayMappingType === ArrayMappingType.ArrayToArray) { codeActions.push({ - title: "Map with array function", - onClick: onClickMapViaArrayFn + title: "Map with query expression", + onClick: onClickMapWithQuery }); } else if (arrayMappingType === ArrayMappingType.ArrayToSingleton) { // TODO: Add impl diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Label/QueryExprLabelWidget.tsx b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Label/QueryExprLabelWidget.tsx new file mode 100644 index 00000000000..53a4489b471 --- /dev/null +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Label/QueryExprLabelWidget.tsx @@ -0,0 +1,123 @@ +/** + * 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/inline-data-mapper/src/components/Diagram/Link/DataMapperLink/DataMapperLink.ts b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Link/DataMapperLink/DataMapperLink.ts index 8f24375209b..4abf821f5cd 100644 --- a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Link/DataMapperLink/DataMapperLink.ts +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Link/DataMapperLink/DataMapperLink.ts @@ -30,7 +30,8 @@ export class DataMapperLinkModel extends DefaultLinkModel { public value?: string, public diagnostics: IDMDiagnostic[] = [], public isActualLink: boolean = false, - public notContainsLabel?: boolean + public notContainsLabel?: boolean, + public isDashLink?: boolean ) { super({ type: LINK_TYPE_ID, diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Link/DataMapperLink/DefaultLinkSegmentWidget.tsx b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Link/DataMapperLink/DefaultLinkSegmentWidget.tsx index c56c9b9fa1a..68fef63e9dc 100644 --- a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Link/DataMapperLink/DefaultLinkSegmentWidget.tsx +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Link/DataMapperLink/DefaultLinkSegmentWidget.tsx @@ -37,7 +37,8 @@ export class DefaultLinkSegmentWidget extends React.Component { if (recordFieldElement) { const fieldId = (recordFieldElement.id.split("-"))[1] + ".OUT"; const portModel = (element as any).getPort(fieldId) as InputOutputPortModel; - if (portModel) { + if (portModel.attributes.portType === "OUT" && + !portModel.attributes?.parentModel && + portModel.attributes?.collapsed + ) { + handleExpand(portModel.attributes.fieldFQN, false); + } else if (portModel) { element = portModel; } } @@ -83,7 +90,7 @@ export class CreateLinkState extends State { if (element instanceof PortModel && !this.sourcePort) { if (element instanceof InputOutputPortModel) { - if (element.portType === "OUT") { + if (element.attributes.portType === "OUT") { this.sourcePort = element; element.fireEvent({}, "mappingStartedFrom"); element.linkedPorts.forEach((linkedPort) => { @@ -93,7 +100,7 @@ export class CreateLinkState extends State { link.setSourcePort(this.sourcePort); link.addLabel(new ExpressionLabelModel({ value: undefined, - context: undefined + context: (element.getNode() as DataMapperNodeModel).context })); this.link = link; } else if (!isValueConfig) { @@ -104,7 +111,7 @@ export class CreateLinkState extends State { } } else if (element instanceof PortModel && this.sourcePort && element !== this.sourcePort) { if ((element instanceof InputOutputPortModel)) { - if (element.portType === "IN") { + if (element.attributes.portType === "IN") { let isDisabled = false; if (element instanceof InputOutputPortModel) { isDisabled = element.isDisabled(); diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/LinkState/DefaultState.ts b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/LinkState/DefaultState.ts index df16168dde4..a0b1cc22a0c 100644 --- a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/LinkState/DefaultState.ts +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/LinkState/DefaultState.ts @@ -28,7 +28,7 @@ import { import { DiagramEngine, DragDiagramItemsState, PortModel } from '@projectstorm/react-diagrams-core'; import { DMCanvasContainerID } from "../Canvas/DataMapperCanvasWidget"; -import { ArrayOutputNode, InputNode, ObjectOutputNode } from '../Node'; +import { ArrayOutputNode, InputNode, ObjectOutputNode, QueryOutputNode } from '../Node'; import { DataMapperNodeModel } from "../Node/commons/DataMapperNode"; import { LinkOverayContainerID } from '../OverriddenLinkLayer/LinkOverlayPortal'; import { CreateLinkState } from './CreateLinkState'; @@ -41,7 +41,6 @@ export class DefaultState extends State { constructor(resetState: boolean = false) { super({ name: 'starting-state' }); - this.childStates = [new SelectingState()]; this.dragCanvas = new DragCanvasState({allowDrag: false}); this.createLink = new CreateLinkState(resetState); this.dragItems = new DragDiagramItemsState(); @@ -94,6 +93,7 @@ export class DefaultState extends State { && (element instanceof PortModel || element instanceof ObjectOutputNode || element instanceof ArrayOutputNode + || element instanceof QueryOutputNode || element instanceof InputNode ) ) { diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/ArrayOutput/ArrayOuptutFieldWidget.tsx b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/ArrayOutput/ArrayOuptutFieldWidget.tsx index 445074eb9aa..5caf3e44964 100644 --- a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/ArrayOutput/ArrayOuptutFieldWidget.tsx +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/ArrayOutput/ArrayOuptutFieldWidget.tsx @@ -24,7 +24,7 @@ import { IOType, TypeKind } from "@wso2/ballerina-core"; import classnames from "classnames"; import { useIONodesStyles } from "../../../styles"; -import { useDMCollapsedFieldsStore, useDMExpressionBarStore } from '../../../../store/store'; +import { useDMCollapsedFieldsStore, useDMExpandedFieldsStore, useDMExpressionBarStore } from '../../../../store/store'; import { useDMSearchStore } from "../../../../store/store"; import { IDataMapperContext } from "../../../../utils/DataMapperContext/DataMapperContext"; import { DataMapperPortWidget, PortState, InputOutputPortModel } from "../../Port"; @@ -40,6 +40,7 @@ import FieldActionWrapper from "../commons/FieldActionWrapper"; import { addValue, removeMapping } from "../../utils/modification-utils"; import { PrimitiveOutputElementWidget } from "../PrimitiveOutput/PrimitiveOutputElementWidget"; import { OutputBeforeInputNotification } from "../commons/OutputBeforeInputNotification"; +import { OutputFieldPreviewWidget } from "./OutputFieldPreviewWidget"; export interface ArrayOutputFieldWidgetProps { parentId: string; @@ -73,6 +74,7 @@ export function ArrayOutputFieldWidget(props: ArrayOutputFieldWidgetProps) { const [hasOutputBeforeInput, setHasOutputBeforeInput] = useState(false); const [isAddingElement, setIsAddingElement] = useState(false); const collapsedFieldsStore = useDMCollapsedFieldsStore(); + const expandedFieldsStore = useDMExpandedFieldsStore(); const setExprBarFocusedPort = useDMExpressionBarStore(state => state.setFocusedPort); const arrayField = field.member; @@ -85,14 +87,14 @@ export function ArrayOutputFieldWidget(props: ArrayOutputFieldWidgetProps) { const fieldName = field?.variableName || ''; const portIn = getPort(`${portName}.IN`); - const mapping = portIn && portIn.value; + const mapping = portIn && portIn.attributes.value; const { inputs, expression, elements, diagnostics } = mapping || {}; const searchValue = useDMSearchStore.getState().outputSearch; const hasElements = elements?.length > 0 && elements.some((element) => element.mappings.length > 0); const connectedViaLink = inputs?.length > 0; let expanded = true; - if (portIn && portIn.collapsed) { + if (portIn && portIn.attributes.collapsed) { expanded = false; } @@ -102,17 +104,18 @@ export function ArrayOutputFieldWidget(props: ArrayOutputFieldWidgetProps) { } const hasDefaultValue = expression && getDefaultValue(field.kind) === expression.trim(); - let isDisabled = portIn.descendantHasValue; + let isDisabled = portIn.attributes.descendantHasValue; if (!isDisabled && !hasDefaultValue) { - if (hasElements && expanded && portIn.parentModel) { + if (hasElements && expanded && portIn.attributes.parentModel) { portIn.setDescendantHasValue(); isDisabled = true; } - if (portIn.parentModel - && (Object.entries(portIn.parentModel.links).length > 0 || portIn.parentModel.ancestorHasValue) + if (portIn.attributes.parentModel && ( + Object.entries(portIn.attributes.parentModel.links).length > 0 + || portIn.attributes.parentModel.attributes.ancestorHasValue) ) { - portIn.ancestorHasValue = true; + portIn.attributes.ancestorHasValue = true; isDisabled = true; } } @@ -139,14 +142,14 @@ export function ArrayOutputFieldWidget(props: ArrayOutputFieldWidgetProps) { {fieldName} {!field?.optional && *} {fieldName && typeName && ":"} {typeName && ( - + {typeName} )} @@ -265,11 +268,14 @@ export function ArrayOutputFieldWidget(props: ArrayOutputFieldWidgetProps) { }, [isAddingElement]); const handleExpand = () => { + const expandedFields = expandedFieldsStore.fields; const collapsedFields = collapsedFieldsStore.fields; - if (!expanded) { - collapsedFieldsStore.setFields(collapsedFields.filter((element) => element !== portName)); - } else { + if (expanded) { + expandedFieldsStore.setFields(expandedFields.filter((element) => element !== portName)); collapsedFieldsStore.setFields([...collapsedFields, portName]); + } else { + expandedFieldsStore.setFields([...expandedFields, portName]); + collapsedFieldsStore.setFields(collapsedFields.filter((element) => element !== portName)); } }; @@ -292,10 +298,14 @@ export function ArrayOutputFieldWidget(props: ArrayOutputFieldWidgetProps) { }; const handleAddArrayElement = async () => { + const varName = context.views[0].targetField; + const viewId = context.views[context.views.length - 1].targetField; + setIsAddingElement(true) try { - return await context.addArrayElement(`${mapping.output}`); + await context.addArrayElement(`${mapping.output}`, `${viewId}`, `${varName}`); } finally { + if (!expanded) handleExpand(); setIsAddingElement(false); } }; @@ -311,6 +321,7 @@ export function ArrayOutputFieldWidget(props: ArrayOutputFieldWidgetProps) { const valConfigMenuItems: ValueConfigMenuItem[] = hasElements || hasDefaultValue ? [ // { title: ValueConfigOption.EditValue, onClick: handleEditValue }, // TODO: Enable this after adding support for editing array values + { title: ValueConfigOption.AddElement, onClick: handleAddArrayElement }, { title: ValueConfigOption.DeleteArray, onClick: handleArrayDeletion }, ] : [ @@ -345,7 +356,7 @@ export function ArrayOutputFieldWidget(props: ArrayOutputFieldWidgetProps) { )} - {(expression && !connectedViaLink) && ( + {(!connectedViaLink) && ( + )} + {label} + + + + {fields && expanded && + fields.map((subField, index) => { + return ( + + ); + }) + } + + ); +} diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/EmptyInputs/EmptyInputsNodeWidget.tsx b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/EmptyInputs/EmptyInputsNodeWidget.tsx index 3ec7078c066..0183553c82f 100644 --- a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/EmptyInputs/EmptyInputsNodeWidget.tsx +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/EmptyInputs/EmptyInputsNodeWidget.tsx @@ -28,9 +28,10 @@ const useStyles = () => ({ width: `${IO_NODE_DEFAULT_WIDTH}px`, cursor: "default", padding: "16px", - backgroundColor: "var(--vscode-input-background)", - border: "1px dashed var(--vscode-input-border)", - borderRadius: "4px" + fontFamily: "GilmerRegular", + background: "var(--vscode-sideBar-background)", + border: "1.8px dashed var(--vscode-dropdown-border)", + borderRadius: "6px" }), unsupportedIOBanner: css({ padding: "12px" @@ -48,6 +49,8 @@ const useStyles = () => ({ marginRight: '8px' }), messageTitle: css({ + color: "var(--vscode-foreground)", + opacity: 0.9, fontWeight: 500 }), divider: css({ diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/Input/InputNode.ts b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/Input/InputNode.ts index 37696b11e22..d65020f807d 100644 --- a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/Input/InputNode.ts +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/Input/InputNode.ts @@ -17,15 +17,17 @@ */ import { Point } from "@projectstorm/geometry"; -import { useDMCollapsedFieldsStore } from "../../../../store/store"; +import { useDMCollapsedFieldsStore, useDMExpandedFieldsStore, useDMSearchStore } from "../../../../store/store"; import { IDataMapperContext } from "../../../../utils/DataMapperContext/DataMapperContext"; import { DataMapperNodeModel } from "../commons/DataMapperNode"; import { IOType, TypeKind } from "@wso2/ballerina-core"; +import { getSearchFilteredInput } from "../../utils/search-utils"; export const INPUT_NODE_TYPE = "datamapper-node-input"; const NODE_ID = "input-node"; export class InputNode extends DataMapperNodeModel { + public filteredInputType: IOType; public numberOfFields: number; public x: number; private identifier: string; @@ -33,6 +35,7 @@ export class InputNode extends DataMapperNodeModel { constructor( public context: IDataMapperContext, public inputType: IOType, + public hasNoMatchingFields?: boolean ) { super( NODE_ID, @@ -44,25 +47,53 @@ export class InputNode extends DataMapperNodeModel { } async initPorts() { + this.filteredInputType = this.getSearchFilteredType(); + this.hasNoMatchingFields = !this.filteredInputType; this.numberOfFields = 1; - if (this.inputType) { + if (this.filteredInputType) { const collapsedFields = useDMCollapsedFieldsStore.getState().fields; - const parentPort = this.addPortsForHeader(this.inputType, this.identifier, "OUT", undefined, undefined); + const expandedFields = useDMExpandedFieldsStore.getState().fields; + const focusedFieldFQNs = this.context.model.query?.inputs || []; + const parentPort = this.addPortsForHeader({ + dmType: this.filteredInputType, + name: this.identifier, + portType: "OUT", + portPrefix: undefined, + focusedFieldFQNs, + collapsedFields, + expandedFields + }); - if (this.inputType.kind === TypeKind.Record) { - const fields = this.inputType.fields; + if (this.filteredInputType.kind === TypeKind.Record) { + const fields = this.filteredInputType.fields; fields.forEach((subField) => { - this.numberOfFields += this.addPortsForInputField( - subField, "OUT", this.identifier, this.identifier, '', - parentPort, collapsedFields, parentPort.collapsed, subField.optional - ); + this.numberOfFields += this.addPortsForInputField({ + field: subField, + portType: "OUT", + parentId: this.identifier, + unsafeParentId: this.identifier, + parent: parentPort, + collapsedFields, + expandedFields, + hidden: parentPort.attributes.collapsed, + isOptional: subField.optional, + focusedFieldFQNs + }); }); } else { - this.addPortsForInputField( - this.inputType, "OUT", this.identifier, this.identifier, '', - parentPort, collapsedFields, parentPort.collapsed, this.inputType.optional - ); + this.addPortsForInputField({ + field: this.filteredInputType, + portType: "OUT", + parentId: this.identifier, + unsafeParentId: this.identifier, + parent: parentPort, + collapsedFields, + expandedFields, + hidden: parentPort.attributes.collapsed, + isOptional: this.filteredInputType.optional, + focusedFieldFQNs + }); } } } @@ -71,6 +102,20 @@ export class InputNode extends DataMapperNodeModel { // Links are always created from "IN" ports by backtracing the inputs. } + public getSearchFilteredType() { + // TODO: Include variableName for inputTypes (Currently only available for variables) + const variableName = this.inputType.variableName || this.inputType.id; + if (variableName) { + const searchValue = useDMSearchStore.getState().inputSearch; + + const matchesParamName = variableName.includes(searchValue?.toLowerCase()); + const type = matchesParamName + ? this.inputType + : getSearchFilteredInput(this.inputType, variableName); + return type; + } + } + setPosition(point: Point): void; setPosition(x: number, y: number): void; setPosition(x: unknown, y?: unknown): void { diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/Input/InputNodeFactory.tsx b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/Input/InputNodeFactory.tsx index 585ca4f6bf9..0d7fa691443 100644 --- a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/Input/InputNodeFactory.tsx +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/Input/InputNodeFactory.tsx @@ -34,27 +34,28 @@ export class InputNodeFactory extends AbstractReactFactory ); - } else if (event.model.inputType && event.model.inputType.kind === TypeKind.Record) { + } else if (event.model.filteredInputType && event.model.filteredInputType.kind === TypeKind.Record) { return ( event.model.getPort(portId) as InputOutputPortModel} + focusedInputs={event.model.context.model.query ? event.model.context.model.query.inputs : []} /> ); } return ( event.model.getPort(portId) as InputOutputPortModel} - valueLabel={event.model.inputType?.variableName} + valueLabel={event.model.filteredInputType?.variableName} /> ) } diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/Input/InputNodeTreeItemWidget.tsx b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/Input/InputNodeTreeItemWidget.tsx index 32dd537563f..da57f3b6295 100644 --- a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/Input/InputNodeTreeItemWidget.tsx +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/Input/InputNodeTreeItemWidget.tsx @@ -19,14 +19,14 @@ import React, { useState } from "react"; import { DiagramEngine } from "@projectstorm/react-diagrams-core"; -import { Button, Codicon } from "@wso2/ui-toolkit"; +import { Button, Codicon, Tooltip } from "@wso2/ui-toolkit"; import { IOType, TypeKind } from "@wso2/ballerina-core"; import classnames from "classnames"; import { DataMapperPortWidget, PortState, InputOutputPortModel } from "../../Port"; import { InputSearchHighlight } from "../commons/Search"; import { useIONodesStyles } from "../../../styles"; -import { useDMCollapsedFieldsStore } from '../../../../store/store'; +import { useDMCollapsedFieldsStore, useDMExpandedFieldsStore } from '../../../../store/store'; import { getTypeName } from "../../utils/type-utils"; @@ -37,14 +37,16 @@ export interface InputNodeTreeItemWidgetProps { getPort: (portId: string) => InputOutputPortModel; treeDepth?: number; hasHoveredParent?: boolean; + focusedInputs?: string[]; } export function InputNodeTreeItemWidget(props: InputNodeTreeItemWidgetProps) { - const { parentId, dmType, getPort, engine, treeDepth = 0, hasHoveredParent } = props; + const { parentId, dmType, getPort, engine, treeDepth = 0, hasHoveredParent, focusedInputs } = props; const [ portState, setPortState ] = useState(PortState.Unselected); const [isHovered, setIsHovered] = useState(false); const collapsedFieldsStore = useDMCollapsedFieldsStore(); + const expandedFieldsStore = useDMExpandedFieldsStore(); const fieldName = dmType.variableName; const typeName = getTypeName(dmType); @@ -57,24 +59,25 @@ export function InputNodeTreeItemWidget(props: InputNodeTreeItemWidgetProps) { if (dmType.kind === TypeKind.Record) { fields = dmType.fields; + } else if (dmType.kind === TypeKind.Array) { + fields = [ dmType.member ]; } let expanded = true; - if (portOut && portOut.collapsed) { + if (portOut && portOut.attributes.collapsed) { expanded = false; } const indentation = fields ? 0 : ((treeDepth + 1) * 16) + 8; const label = ( - + {fieldName} {dmType.optional && "?"} - {typeName && ":"} {typeName && ( - + {typeName} )} @@ -83,11 +86,20 @@ export function InputNodeTreeItemWidget(props: InputNodeTreeItemWidgetProps) { ); const handleExpand = () => { - const collapsedFields = collapsedFieldsStore.fields; - if (!expanded) { - collapsedFieldsStore.setFields(collapsedFields.filter((element) => element !== fieldId)); + if (dmType.kind === TypeKind.Array) { + const expandedFields = expandedFieldsStore.fields; + if (expanded) { + expandedFieldsStore.setFields(expandedFields.filter((element) => element !== fieldId)); + } else { + expandedFieldsStore.setFields([...expandedFields, fieldId]); + } } else { - collapsedFieldsStore.setFields([...collapsedFields, fieldId]); + const collapsedFields = collapsedFieldsStore.fields; + if (!expanded) { + collapsedFieldsStore.setFields(collapsedFields.filter((element) => element !== fieldId)); + } else { + collapsedFieldsStore.setFields([...collapsedFields, fieldId]); + } } }; @@ -103,9 +115,8 @@ export function InputNodeTreeItemWidget(props: InputNodeTreeItemWidgetProps) { setIsHovered(false); }; - return ( - <> -
- {portOut && + {portOut && !portOut.attributes.isPreview && }
+ ); + + return ( + <> + {!portOut?.attributes.isPreview ? treeItemHeader : ( + Please map parent field first
)} + sx={{ fontSize: "12px" }} + containerSx={{ width: "100%" }} + > + {treeItemHeader} + + )} {fields && expanded && fields.map((subField, index) => { return ( @@ -143,6 +167,7 @@ export function InputNodeTreeItemWidget(props: InputNodeTreeItemWidgetProps) { parentId={fieldId} treeDepth={treeDepth + 1} hasHoveredParent={isHovered || hasHoveredParent} + focusedInputs={focusedInputs} /> ); }) diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/Input/InputNodeWidget.tsx b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/Input/InputNodeWidget.tsx index 73915a89100..a0d877946e6 100644 --- a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/Input/InputNodeWidget.tsx +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/Input/InputNodeWidget.tsx @@ -20,7 +20,7 @@ import React, { useState } from "react"; import { Button, Codicon } from "@wso2/ui-toolkit"; import { DiagramEngine } from '@projectstorm/react-diagrams'; -import { IOType } from "@wso2/ballerina-core"; +import { IOType, TypeKind } from "@wso2/ballerina-core"; import { DataMapperPortWidget, PortState, InputOutputPortModel } from '../../Port'; import { InputSearchHighlight } from '../commons/Search'; @@ -38,10 +38,11 @@ export interface InputNodeWidgetProps { getPort: (portId: string) => InputOutputPortModel; valueLabel?: string; nodeHeaderSuffix?: string; + focusedInputs?: string[]; } export function InputNodeWidget(props: InputNodeWidgetProps) { - const { engine, dmType, id, getPort, valueLabel, nodeHeaderSuffix } = props; + const { engine, dmType, id, getPort, valueLabel, nodeHeaderSuffix, focusedInputs } = props; const [portState, setPortState] = useState(PortState.Unselected); const [isHovered, setIsHovered] = useState(false); @@ -62,19 +63,26 @@ export function InputNodeWidget(props: InputNodeWidgetProps) { const hasFields = !!dmType?.fields?.length; + let fields: IOType[]; + + if (dmType.kind === TypeKind.Record) { + fields = dmType.fields; + } else if (dmType.kind === TypeKind.Array) { + fields = [ dmType.member ]; + } + let expanded = true; - if (portOut && portOut.collapsed) { + if (portOut && portOut.attributes.collapsed) { expanded = false; } const label = ( - + {valueLabel ? valueLabel : id} - {typeName && ":"} {typeName && ( - + {typeName} )} @@ -151,6 +159,7 @@ export function InputNodeWidget(props: InputNodeWidgetProps) { parentId={id} treeDepth={0} hasHoveredParent={isHovered} + focusedInputs={focusedInputs} /> ); }) diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/LinkConnector/LinkConnectorNode.ts b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/LinkConnector/LinkConnectorNode.ts index e1f3baee33e..79234057865 100644 --- a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/LinkConnector/LinkConnectorNode.ts +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/LinkConnector/LinkConnectorNode.ts @@ -27,6 +27,8 @@ import { IDMDiagnostic, Mapping } from "@wso2/ballerina-core"; import { getTargetPortPrefix } from "../../utils/port-utils"; import { ArrayOutputNode } from "../ArrayOutput"; import { removeMapping } from "../../utils/modification-utils"; +import { QueryOutputNode } from "../QueryOutput"; +import { useDMSearchStore } from "../../../../store/store"; export const LINK_CONNECTOR_NODE_TYPE = "link-connector-node"; const NODE_ID = "link-connector-node"; @@ -43,7 +45,7 @@ export class LinkConnectorNode extends DataMapperNodeModel { public value: string; public diagnostics: IDMDiagnostic[]; public hidden: boolean; - public hasInitialized: boolean; + public shouldInitLinks: boolean; public label: string; constructor( @@ -69,7 +71,15 @@ export class LinkConnectorNode extends DataMapperNodeModel { this.addPort(this.inPort); this.addPort(this.outPort); + const inputSearch = useDMSearchStore.getState().inputSearch; + const outputSearch = useDMSearchStore.getState().outputSearch; + 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); if (inputNode) { const inputPort = getInputPort(inputNode, field.replace(/\.\d+/g, '')); @@ -79,10 +89,13 @@ export class LinkConnectorNode extends DataMapperNodeModel { } }) - if (this.outPort) { + 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) { + 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; @@ -90,13 +103,13 @@ export class LinkConnectorNode extends DataMapperNodeModel { [this.targetPort, this.targetMappedPort] = getOutputPort(node, this.mapping.output); const previouslyHidden = this.hidden; - this.hidden = this.targetMappedPort?.portName !== this.targetPort?.portName; + 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.hasInitialized = false; + this.shouldInitLinks = true; } } }); @@ -104,7 +117,7 @@ export class LinkConnectorNode extends DataMapperNodeModel { } initLinks(): void { - if (this.hasInitialized) { + if (!this.shouldInitLinks) { return; } if (this.hidden) { @@ -131,7 +144,7 @@ export class LinkConnectorNode extends DataMapperNodeModel { this.getModel().addAll(lm as any); if (!this.label) { - this.label = this.targetMappedPort.fieldFQN.split('.').pop(); + this.label = this.targetMappedPort.attributes.fieldFQN.split('.').pop(); } }) } @@ -182,13 +195,13 @@ export class LinkConnectorNode extends DataMapperNodeModel { }) if (!this.label) { - const fieldFQN = this.targetMappedPort.fieldFQN; - this.label = fieldFQN ? this.targetMappedPort.fieldFQN.split('.').pop() : ''; + const fieldFQN = this.targetMappedPort.attributes.fieldFQN; + this.label = fieldFQN ? this.targetMappedPort.attributes.fieldFQN.split('.').pop() : ''; } this.getModel().addAll(lm as any); } } - this.hasInitialized = true; + this.shouldInitLinks = false; } async updateSource(suffix: string): Promise { diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/LinkConnector/LinkConnectorNodeWidget.tsx b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/LinkConnector/LinkConnectorNodeWidget.tsx index 78f60ab7b05..fcd2216a0ea 100644 --- a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/LinkConnector/LinkConnectorNodeWidget.tsx +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/LinkConnector/LinkConnectorNodeWidget.tsx @@ -21,6 +21,7 @@ import React, { useState } from "react"; import { DiagramEngine } from '@projectstorm/react-diagrams'; import { ProgressRing } from '@wso2/ui-toolkit'; import classnames from "classnames"; +import { useShallow } from "zustand/react/shallow"; import { LinkConnectorNode } from './LinkConnectorNode'; import { useIntermediateNodeStyles } from '../../../styles'; @@ -28,7 +29,7 @@ import { DiagnosticWidget } from '../../Diagnostic/DiagnosticWidget'; import { renderDeleteButton, renderEditButton, renderPortWidget } from './LinkConnectorWidgetComponents'; import { useDMExpressionBarStore } from "../../../../store/store"; import { InputOutputPortModel } from "../../Port"; -import { useShallow } from "zustand/react/shallow"; + export interface LinkConnectorNodeWidgetProps { node: LinkConnectorNode; @@ -39,7 +40,6 @@ export function LinkConnectorNodeWidget(props: LinkConnectorNodeWidgetProps) { 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; @@ -47,8 +47,7 @@ export function LinkConnectorNodeWidget(props: LinkConnectorNodeWidgetProps) { const [deleteInProgress, setDeleteInProgress] = useState(false); const onClickEdit = () => { - const targetPort = node.targetMappedPort; - setExprBarFocusedPort(targetPort as InputOutputPortModel); + // TODO: Focus the expression editor }; const onClickDelete = async () => { diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/ObjectOutput/ObjectOutputFieldWidget.tsx b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/ObjectOutput/ObjectOutputFieldWidget.tsx index f316a65e9f0..bdcb344b4d3 100644 --- a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/ObjectOutput/ObjectOutputFieldWidget.tsx +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/ObjectOutput/ObjectOutputFieldWidget.tsx @@ -36,6 +36,7 @@ import FieldActionWrapper from "../commons/FieldActionWrapper"; import { ValueConfigMenu, ValueConfigMenuItem, ValueConfigOption } from "../commons/ValueConfigButton"; import { DiagnosticTooltip } from "../../Diagnostic/DiagnosticTooltip"; import { OutputBeforeInputNotification } from "../commons/OutputBeforeInputNotification"; +import { DataMapperLinkModel } from "../../Link"; export interface ObjectOutputFieldWidgetProps { parentId: string; @@ -83,7 +84,7 @@ export function ObjectOutputFieldWidget(props: ObjectOutputFieldWidgetProps) { let fieldName = field?.variableName || ''; let portName = updatedParentId !== '' ? fieldName !== '' ? `${updatedParentId}.${fieldName}` : updatedParentId : fieldName; const portIn = getPort(portName + ".IN"); - const mapping = portIn && portIn.value; + const mapping = portIn && portIn.attributes.value; const { inputs, expression, diagnostics } = mapping || {}; const connectedViaLink = inputs?.length > 0; const hasDefaultValue = expression && getDefaultValue(field.kind) === expression.trim(); @@ -139,18 +140,20 @@ export function ObjectOutputFieldWidget(props: ObjectOutputFieldWidgetProps) { setIsHovered(false); }; - let isDisabled = portIn?.descendantHasValue; + let isDisabled = portIn?.attributes.descendantHasValue; if (!isDisabled) { - if (portIn?.parentModel - && (Object.entries(portIn?.parentModel.links).length > 0 || portIn?.parentModel.ancestorHasValue) + if (portIn?.attributes.parentModel && ( + Object.values(portIn?.attributes.parentModel.links) + .filter((link)=> !(link as DataMapperLinkModel).isDashLink).length > 0 || + portIn?.attributes.parentModel.attributes.ancestorHasValue) ) { - portIn.ancestorHasValue = true; + portIn.attributes.ancestorHasValue = true; isDisabled = true; } } - if (portIn && portIn.collapsed) { + if (portIn && portIn.attributes.collapsed) { expanded = false; } @@ -172,11 +175,10 @@ export function ObjectOutputFieldWidget(props: ObjectOutputFieldWidgetProps) { > {fieldName} {!field?.optional && *} - {typeName && ":"} {typeName && ( diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/ObjectOutput/ObjectOutputNode.ts b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/ObjectOutput/ObjectOutputNode.ts index d15e7b35a5e..6c3903d0201 100644 --- a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/ObjectOutput/ObjectOutputNode.ts +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/ObjectOutput/ObjectOutputNode.ts @@ -18,13 +18,12 @@ import { Point } from "@projectstorm/geometry"; import { IOType, Mapping, TypeKind } from "@wso2/ballerina-core"; -import { useDMCollapsedFieldsStore, useDMSearchStore } from "../../../../store/store"; +import { useDMCollapsedFieldsStore, useDMExpandedFieldsStore, useDMSearchStore } from "../../../../store/store"; import { IDataMapperContext } from "../../../../utils/DataMapperContext/DataMapperContext"; import { DataMapperNodeModel } from "../commons/DataMapperNode"; import { getFilteredMappings, getSearchFilteredOutput, hasNoOutputMatchFound } from "../../utils/search-utils"; import { getTypeName } from "../../utils/type-utils"; import { OBJECT_OUTPUT_TARGET_PORT_PREFIX } from "../../utils/constants"; -import { STNode } from "@wso2/syntax-tree"; import { findInputNode } from "../../utils/node-utils"; import { InputOutputPortModel } from "../../Port"; import { DataMapperLinkModel } from "../../Link"; @@ -37,7 +36,7 @@ const NODE_ID = "object-output-node"; export class ObjectOutputNode extends DataMapperNodeModel { public filteredOutputType: IOType; - public filterdMappings: Mapping[]; + public filteredMappings: Mapping[]; public typeName: string; public rootName: string; public hasNoMatchingFields: boolean; @@ -63,23 +62,36 @@ export class ObjectOutputNode extends DataMapperNodeModel { this.rootName = this.filteredOutputType?.id; const collapsedFields = useDMCollapsedFieldsStore.getState().fields; + const expandedFields = useDMExpandedFieldsStore.getState().fields; this.typeName = getTypeName(this.filteredOutputType); this.hasNoMatchingFields = hasNoOutputMatchFound(this.outputType, this.filteredOutputType); - const parentPort = this.addPortsForHeader( - this.filteredOutputType, this.rootName, "IN", OBJECT_OUTPUT_TARGET_PORT_PREFIX, - this.context.model.mappings, this.isMapFn - ); + const parentPort = this.addPortsForHeader({ + dmType: this.filteredOutputType, + name: this.rootName, + portType: "IN", + portPrefix: OBJECT_OUTPUT_TARGET_PORT_PREFIX, + mappings: this.context.model.mappings, + collapsedFields, + expandedFields + }); if (this.filteredOutputType.kind === TypeKind.Record) { if (this.filteredOutputType.fields.length) { this.filteredOutputType.fields.forEach(field => { if (!field) return; - this.addPortsForOutputField( - field, "IN", this.rootName, this.context.model.mappings, OBJECT_OUTPUT_TARGET_PORT_PREFIX, - parentPort, collapsedFields, parentPort.collapsed - ); + this.addPortsForOutputField({ + field, + type: "IN", + parentId: this.rootName, + mappings: this.context.model.mappings, + portPrefix: OBJECT_OUTPUT_TARGET_PORT_PREFIX, + parent: parentPort, + collapsedFields, + expandedFields, + hidden: parentPort.attributes.collapsed + }); }); } } @@ -87,15 +99,16 @@ export class ObjectOutputNode extends DataMapperNodeModel { } initLinks(): void { - const searchValue = useDMSearchStore.getState().outputSearch; - this.filterdMappings = getFilteredMappings(this.context.model.mappings, searchValue); - this.createLinks(this.filterdMappings); + const inputSearch = useDMSearchStore.getState().inputSearch; + const outputSearch = useDMSearchStore.getState().outputSearch; + this.filteredMappings = getFilteredMappings(this.context.model.mappings, inputSearch, outputSearch); + this.createLinks(this.filteredMappings); } private createLinks(mappings: Mapping[]) { mappings.forEach((mapping) => { - const { isComplex, inputs, output, expression, diagnostics } = mapping; - if (isComplex || inputs.length !== 1) { + const { isComplex, isQueryExpression, inputs, output, expression, diagnostics } = mapping; + if (isComplex || isQueryExpression || inputs.length !== 1) { // Complex mappings are handled in the LinkConnectorNode return; } @@ -118,6 +131,7 @@ export class ObjectOutputNode extends DataMapperNodeModel { new ExpressionLabelModel({ value: expression, link: lm, + context: this.context, deleteLink: () => this.deleteField(output), } )); diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/ObjectOutput/ObjectOutputNodeFactory.tsx b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/ObjectOutput/ObjectOutputNodeFactory.tsx index 4d371f8f06d..7af710b67ac 100644 --- a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/ObjectOutput/ObjectOutputNodeFactory.tsx +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/ObjectOutput/ObjectOutputNodeFactory.tsx @@ -42,13 +42,13 @@ export class ObjectOutputNodeFactory extends AbstractReactFactory event.model.getPort(portId) as InputOutputPortModel} context={event.model.context} - mappings={event.model.filterdMappings} - valueLabel={event.model.outputType.id} + mappings={event.model.filteredMappings} + valueLabel={event.model.filteredOutputType.id} originalTypeName={event.model.filteredOutputType?.variableName} /> )} diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/ObjectOutput/ObjectOutputWidget.tsx b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/ObjectOutput/ObjectOutputWidget.tsx index e6922d9ffc4..fc19713893f 100644 --- a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/ObjectOutput/ObjectOutputWidget.tsx +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/ObjectOutput/ObjectOutputWidget.tsx @@ -78,10 +78,10 @@ export function ObjectOutputWidget(props: ObjectOutputWidgetProps) { const portIn = getPort(`${id}.IN`); let expanded = true; - if ((portIn && portIn.collapsed)) { + if ((portIn && portIn.attributes.collapsed)) { expanded = false; } - const isDisabled = portIn?.descendantHasValue; + const isDisabled = portIn?.attributes.descendantHasValue; const indentation = (portIn && (!hasFields || !expanded)) ? 0 : 24; @@ -113,12 +113,11 @@ export function ObjectOutputWidget(props: ObjectOutputWidgetProps) { const label = ( {valueLabel && ( - + {valueLabel} - {typeName && ":"} )} - + {typeName || ''} diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/PrimitiveOutput/PrimitiveOutputElementWidget.tsx b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/PrimitiveOutput/PrimitiveOutputElementWidget.tsx index d48b557c912..567e34f0ef5 100644 --- a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/PrimitiveOutput/PrimitiveOutputElementWidget.tsx +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/PrimitiveOutput/PrimitiveOutputElementWidget.tsx @@ -71,22 +71,21 @@ export function PrimitiveOutputElementWidget(props: PrimitiveOutputElementWidget const [portState, setPortState] = useState(PortState.Unselected); const [hasOutputBeforeInput, setHasOutputBeforeInput] = useState(false); - const typeName = field.kind; - const fieldName = field?.variableName || ''; + const fieldName = field?.id || ''; let portName = parentId; if (fieldIndex !== undefined) { portName = `${parentId}.${fieldIndex}${fieldName !== '' ? `.${fieldName}` : ''}`; } else if (fieldName) { - portName = `${parentId}.${typeName}.${fieldName}`; + portName = `${parentId}.${fieldName}`; } else { - portName = `${parentId}.${typeName}`; + portName = parentId; } const portIn = getPort(`${portName}.IN`); const isExprBarFocused = exprBarFocusedPort?.getName() === portIn?.getName(); - const mapping = portIn && portIn.value; + const mapping = portIn && portIn.attributes.value; const { expression, diagnostics } = mapping || {}; const handleEditValue = () => { diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/PrimitiveOutput/PrimitiveOutputNode.ts b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/PrimitiveOutput/PrimitiveOutputNode.ts new file mode 100644 index 00000000000..4e258ea0b46 --- /dev/null +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/PrimitiveOutput/PrimitiveOutputNode.ts @@ -0,0 +1,164 @@ +/** + * 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 { Point } from "@projectstorm/geometry"; +import { IOType, Mapping } from "@wso2/ballerina-core"; + +import { useDMCollapsedFieldsStore, useDMExpandedFieldsStore, useDMSearchStore } from "../../../../store/store"; +import { IDataMapperContext } from "../../../../utils/DataMapperContext/DataMapperContext"; +import { DataMapperNodeModel } from "../commons/DataMapperNode"; +import { getFilteredMappings, getSearchFilteredOutput, hasNoOutputMatchFound } from "../../utils/search-utils"; +import { getTypeName } from "../../utils/type-utils"; +import { PRIMITIVE_OUTPUT_TARGET_PORT_PREFIX } from "../../utils/constants"; +import { findInputNode } from "../../utils/node-utils"; +import { InputOutputPortModel } from "../../Port"; +import { DataMapperLinkModel } from "../../Link"; +import { ExpressionLabelModel } from "../../Label"; +import { getInputPort, getOutputPort } from "../../utils/port-utils"; +import { removeMapping } from "../../utils/modification-utils"; +import { findMappingByOutput } from "../../utils/common-utils"; + +export const PRIMITIVE_OUTPUT_NODE_TYPE = "data-mapper-node-primitive-output"; +const NODE_ID = "primitive-output-node"; + +export class PrimitiveOutputNode extends DataMapperNodeModel { + public filteredMappings: Mapping[]; + public typeName: string; + public rootName: string; + public hasNoMatchingFields: boolean; + public x: number; + public y: number; + public isMapFn: boolean; + + constructor( + public context: IDataMapperContext, + public outputType: IOType + ) { + super( + NODE_ID, + context, + PRIMITIVE_OUTPUT_NODE_TYPE + ); + } + + async initPorts() { + if (this.outputType) { + const collapsedFields = useDMCollapsedFieldsStore.getState().fields; + const expandedFields = useDMExpandedFieldsStore.getState().fields; + this.typeName = getTypeName(this.outputType); + this.rootName = this.outputType.id; + + const searchValue = useDMSearchStore.getState().outputSearch; + this.hasNoMatchingFields = searchValue && + !findMappingByOutput(this.context.model.mappings, this.outputType.id)?.expression.includes(searchValue); + + const parentPort = this.addPortsForHeader({ + dmType: this.outputType, + name: "", + portType: "IN", + portPrefix: PRIMITIVE_OUTPUT_TARGET_PORT_PREFIX, + mappings: this.context.model.mappings + }); + + this.addPortsForOutputField({ + field: this.outputType, + type: "IN", + parentId: this.rootName, + mappings: this.context.model.mappings, + portPrefix: PRIMITIVE_OUTPUT_TARGET_PORT_PREFIX, + parent: parentPort, + collapsedFields, + expandedFields, + hidden: parentPort.attributes.collapsed + }); + } + } + + initLinks(): void { + const inputSearch = useDMSearchStore.getState().inputSearch; + const outputSearch = useDMSearchStore.getState().outputSearch; + this.filteredMappings = getFilteredMappings(this.context.model.mappings, inputSearch, outputSearch); + this.createLinks(this.filteredMappings); + } + + private createLinks(mappings: Mapping[]) { + mappings.forEach((mapping) => { + const { isComplex, isQueryExpression, inputs, output, expression, diagnostics } = mapping; + if (isComplex || isQueryExpression || inputs.length !== 1) { + // Complex mappings are handled in the LinkConnectorNode + return; + } + + const inputNode = findInputNode(inputs[0], this); + let inPort: InputOutputPortModel; + if (inputNode) { + inPort = getInputPort(inputNode, inputs[0].replace(/\.\d+/g, '')); + } + + const [_, mappedOutPort] = getOutputPort(this, output); + + if (inPort && mappedOutPort) { + const lm = new DataMapperLinkModel(expression, diagnostics, true, undefined); + lm.setTargetPort(mappedOutPort); + lm.setSourcePort(inPort); + inPort.addLinkedPort(mappedOutPort); + + lm.addLabel( + new ExpressionLabelModel({ + value: expression, + link: lm, + deleteLink: () => this.deleteField(output), + } + )); + + lm.registerListener({ + selectionChanged(event) { + if (event.isSelected) { + inPort.fireEvent({}, "link-selected"); + mappedOutPort.fireEvent({}, "link-selected"); + } else { + inPort.fireEvent({}, "link-unselected"); + mappedOutPort.fireEvent({}, "link-unselected"); + } + }, + }); + + this.getModel().addAll(lm as any); + } + }); + } + + async deleteField(field: string) { + await removeMapping(field, this.context); + } + + public updatePosition() { + this.setPosition(this.position.x, this.position.y); + } + + setPosition(point: Point): void; + setPosition(x: number, y: number): void; + setPosition(x: unknown, y?: unknown): void { + if (typeof x === 'number' && typeof y === 'number') { + if (!this.x || !this.y) { + this.x = x; + this.y = y; + } + super.setPosition(x, y); + } + } +} diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/PrimitiveOutput/PrimitiveOutputNodeFactory.tsx b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/PrimitiveOutput/PrimitiveOutputNodeFactory.tsx new file mode 100644 index 00000000000..ffafa60bcfd --- /dev/null +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/PrimitiveOutput/PrimitiveOutputNodeFactory.tsx @@ -0,0 +1,59 @@ +/** + * 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-lambda jsx-no-multiline-js +import * as React from 'react'; + +import { AbstractReactFactory } from '@projectstorm/react-canvas-core'; +import { DiagramEngine } from '@projectstorm/react-diagrams-core'; + +import { InputOutputPortModel } from '../../Port'; +import { PRIMITIVE_OUTPUT_TARGET_PORT_PREFIX } from '../../utils/constants'; +import { PrimitiveOutputWidget } from "./PrimitiveOutputWidget"; +import { OutputSearchNoResultFound, SearchNoResultFoundKind } from "../commons/Search"; + +import { PrimitiveOutputNode, PRIMITIVE_OUTPUT_NODE_TYPE } from './PrimitiveOutputNode'; + +export class PrimitiveOutputNodeFactory extends AbstractReactFactory { + constructor() { + super(PRIMITIVE_OUTPUT_NODE_TYPE); + } + + generateReactWidget(event: { model: PrimitiveOutputNode; }): JSX.Element { + return ( + <> + {event.model.hasNoMatchingFields ? ( + + ) : ( + event.model.getPort(portId) as InputOutputPortModel} + context={event.model.context} + valueLabel={event.model.outputType.id} + /> + )} + + ); + } + + generateModel(): PrimitiveOutputNode { + return undefined; + } +} diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/PrimitiveOutput/PrimitiveOutputWidget.tsx b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/PrimitiveOutput/PrimitiveOutputWidget.tsx new file mode 100644 index 00000000000..fcbdb9691db --- /dev/null +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/PrimitiveOutput/PrimitiveOutputWidget.tsx @@ -0,0 +1,174 @@ +/** + * 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'; +import { Button, Codicon } from '@wso2/ui-toolkit'; +import { IOType, Mapping } from '@wso2/ballerina-core'; + +import { IDataMapperContext } from "../../../../utils/DataMapperContext/DataMapperContext"; +import { DataMapperPortWidget, PortState, InputOutputPortModel } from '../../Port'; +import { TreeBody, TreeContainer, TreeHeader } from '../commons/Tree/Tree'; +import { PrimitiveOutputElementWidget } from "./PrimitiveOutputElementWidget"; +import { useIONodesStyles } from '../../../styles'; +import { useDMCollapsedFieldsStore, useDMIOConfigPanelStore } from '../../../../store/store'; +import { OutputSearchHighlight } from '../commons/Search'; +import { OutputBeforeInputNotification } from '../commons/OutputBeforeInputNotification'; + +export interface PrimitiveOutputWidgetProps { + id: string; + outputType: IOType; + typeName: string; + engine: DiagramEngine; + getPort: (portId: string) => InputOutputPortModel; + context: IDataMapperContext; + valueLabel?: string; +} + +export function PrimitiveOutputWidget(props: PrimitiveOutputWidgetProps) { + const { + id, + outputType, + typeName, + engine, + getPort, + context, + valueLabel + } = props; + const classes = useIONodesStyles(); + + const [portState, setPortState] = useState(PortState.Unselected); + const [isHovered, setIsHovered] = useState(false); + const [hasOutputBeforeInput, setHasOutputBeforeInput] = useState(false); + + const collapsedFieldsStore = useDMCollapsedFieldsStore(); + + const { setIsIOConfigPanelOpen, setIOConfigPanelType, setIsSchemaOverridden } = useDMIOConfigPanelStore(state => ({ + setIsIOConfigPanelOpen: state.setIsIOConfigPanelOpen, + setIOConfigPanelType: state.setIOConfigPanelType, + setIsSchemaOverridden: state.setIsSchemaOverridden + })); + + const portIn = getPort(`${id}.IN`); + + let expanded = true; + if ((portIn && portIn.attributes.collapsed)) { + expanded = false; + } + + const isDisabled = portIn?.attributes.descendantHasValue; + const indentation = (portIn && !expanded) ? 0 : 24; + + const handleExpand = () => { + const collapsedFields = collapsedFieldsStore.fields; + if (!expanded) { + collapsedFieldsStore.setFields(collapsedFields.filter((element) => element !== id)); + } else { + collapsedFieldsStore.setFields([...collapsedFields, id]); + } + }; + + const handlePortState = (state: PortState) => { + setPortState(state) + }; + + const handlePortSelection = (outputBeforeInput: boolean) => { + setHasOutputBeforeInput(outputBeforeInput); + }; + + const onMouseEnter = () => { + setIsHovered(true); + }; + + const onMouseLeave = () => { + setIsHovered(false); + }; + + const label = ( + + {valueLabel && ( + + {valueLabel} + + )} + + {typeName || ''} + + + ); + + const onRightClick = (event: React.MouseEvent) => { + event.preventDefault(); + setIOConfigPanelType("Output"); + setIsSchemaOverridden(true); + setIsIOConfigPanelOpen(true); + }; + + return ( + <> + + + + {portIn && ( + ) + } + + + + {label} + + {hasOutputBeforeInput && } + + {(expanded && outputType) && ( + + + + )} + + + ); +} diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/PrimitiveOutput/index.ts b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/PrimitiveOutput/index.ts new file mode 100644 index 00000000000..dac2b4c1868 --- /dev/null +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/PrimitiveOutput/index.ts @@ -0,0 +1,19 @@ +/** + * 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 "./PrimitiveOutputNodeFactory"; +export * from "./PrimitiveOutputNode"; diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/QueryExprConnector/QueryExprConnectorNode.ts b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/QueryExprConnector/QueryExprConnectorNode.ts new file mode 100644 index 00000000000..5549c2216f0 --- /dev/null +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/QueryExprConnector/QueryExprConnectorNode.ts @@ -0,0 +1,225 @@ +/** + * 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 { IDMDiagnostic, 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 QUERY_EXPR_CONNECTOR_NODE_TYPE = "query-expr-connector-node"; +const NODE_ID = "query-expr-connector-node"; + +export class QueryExprConnectorNode extends DataMapperNodeModel { + + public sourcePorts: InputOutputPortModel[] = []; + public targetPort: InputOutputPortModel; + public targetMappedPort: InputOutputPortModel; + + public inPort: IntermediatePortModel; + public outPort: IntermediatePortModel; + + public diagnostics: IDMDiagnostic[]; + public value: string; + public hidden: boolean; + public shouldInitLinks: boolean; + public label: string; + + constructor( + public context: IDataMapperContext, + public mapping: Mapping + ) { + super( + NODE_ID, + context, + QUERY_EXPR_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; + + 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); + 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.QUERY_EXPR_CONNECTOR_NODE_WITH_ERROR.X + : OFFSETS.LINK_CONNECTOR_NODE.X, + position.y - 2 + ); + } + } + + public async deleteLink(): Promise { + await removeMapping(this.mapping.output, this.context); + } + + public hasError(): boolean { + return this.diagnostics.length > 0; + } +} diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/QueryExprConnector/QueryExprConnectorNodeFactory.tsx b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/QueryExprConnector/QueryExprConnectorNodeFactory.tsx new file mode 100644 index 00000000000..1dfd5fb38b2 --- /dev/null +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/QueryExprConnector/QueryExprConnectorNodeFactory.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 { QueryExprConnectorNode, QUERY_EXPR_CONNECTOR_NODE_TYPE } from './QueryExprConnectorNode'; +import { QueryExprConnectorNodeWidget } from './QueryExprConnectorNodeWidget'; + +export class QueryExprConnectorNodeFactory extends AbstractReactFactory { + constructor() { + super(QUERY_EXPR_CONNECTOR_NODE_TYPE); + } + + generateReactWidget(event: { model: QueryExprConnectorNode; }): 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(): QueryExprConnectorNode { + return undefined; + } +} diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/QueryExprConnector/QueryExprConnectorNodeWidget.tsx b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/QueryExprConnector/QueryExprConnectorNodeWidget.tsx new file mode 100644 index 00000000000..7ede618f24c --- /dev/null +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/QueryExprConnector/QueryExprConnectorNodeWidget.tsx @@ -0,0 +1,111 @@ +/** + * 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 { QueryExprConnectorNode } from './QueryExprConnectorNode'; +import { renderDeleteButton, renderEditButton, renderPortWidget } from "../LinkConnector/LinkConnectorWidgetComponents"; +import { DiagnosticWidget } from "../../Diagnostic/DiagnosticWidget"; +import { expandArrayFn } from "../../utils/common-utils"; +import { useDMCollapsedFieldsStore, useDMExpandedFieldsStore } from "../../../../store/store"; + +export interface QueryExprConnectorNodeWidgetWidgetProps { + node: QueryExprConnectorNode; + engine: DiagramEngine; +} + +export function QueryExprConnectorNodeWidget(props: QueryExprConnectorNodeWidgetWidgetProps) { + const { node, engine } = props; + + const classes = useIntermediateNodeStyles(); + 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 = () => { + // TODO: Focus the expression editor + }; + + const onClickDelete = async () => { + setDeleteInProgress(true); + if (node.deleteLink) { + await node.deleteLink(); + } + setDeleteInProgress(false); + }; + + const onFocusQueryExpr = () => { + 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); + + expandArrayFn(node.context, node.targetMappedPort.attributes.value?.output); + }; + + 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`)} +
+
+ ) + ); +} diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/QueryExprConnector/index.ts b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/QueryExprConnector/index.ts new file mode 100644 index 00000000000..b78bb4c975e --- /dev/null +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/QueryExprConnector/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 "./QueryExprConnectorNode"; +export * from "./QueryExprConnectorNodeFactory"; +export * from "./QueryExprConnectorNodeWidget"; diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/QueryOutput/QueryOutputNode.ts b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/QueryOutput/QueryOutputNode.ts new file mode 100644 index 00000000000..c5fcb4052fb --- /dev/null +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/QueryOutput/QueryOutputNode.ts @@ -0,0 +1,196 @@ +/** + * 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 { Point } from "@projectstorm/geometry"; +import { IOType, Mapping, TypeKind } from "@wso2/ballerina-core"; + +import { useDMCollapsedFieldsStore, useDMExpandedFieldsStore, useDMSearchStore } from "../../../../store/store"; +import { IDataMapperContext } from "../../../../utils/DataMapperContext/DataMapperContext"; +import { DataMapperNodeModel } from "../commons/DataMapperNode"; +import { getFilteredMappings, getSearchFilteredOutput, hasNoOutputMatchFound } from "../../utils/search-utils"; +import { getTypeName } from "../../utils/type-utils"; +import { QUERY_OUTPUT_TARGET_PORT_PREFIX } from "../../utils/constants"; +import { findInputNode } from "../../utils/node-utils"; +import { InputOutputPortModel } from "../../Port"; +import { ArrowLinkModel, DataMapperLinkModel } from "../../Link"; +import { ExpressionLabelModel } from "../../Label"; +import { getInputPort, getOutputPort } from "../../utils/port-utils"; +import { removeMapping } from "../../utils/modification-utils"; + +export const QUERY_OUTPUT_NODE_TYPE = "data-mapper-node-query-output"; +const NODE_ID = "query-output-node"; + +export class QueryOutputNode extends DataMapperNodeModel { + public filteredOutputType: IOType; + public filteredMappings: Mapping[]; + public typeName: string; + public rootName: string; + public hasNoMatchingFields: boolean; + public x: number; + public y: number; + public isMapFn: boolean; + + constructor( + public context: IDataMapperContext, + public outputType: IOType + ) { + super( + NODE_ID, + context, + QUERY_OUTPUT_NODE_TYPE + ); + } + + async initPorts() { + this.filteredOutputType = getSearchFilteredOutput(this.outputType); + + if (this.filteredOutputType) { + this.rootName = this.filteredOutputType?.id; + + const collapsedFields = useDMCollapsedFieldsStore.getState().fields; + const expandedFields = useDMExpandedFieldsStore.getState().fields; + this.typeName = getTypeName(this.filteredOutputType); + + this.hasNoMatchingFields = hasNoOutputMatchFound(this.outputType, this.filteredOutputType); + + const parentPort = this.addPortsForHeader({ + dmType: this.filteredOutputType, + name: this.rootName, + portType: "IN", + portPrefix: QUERY_OUTPUT_TARGET_PORT_PREFIX, + mappings: this.context.model.mappings, + collapsedFields, + expandedFields, + isPreview: true + }); + + this.addPortsForOutputField({ + field: this.filteredOutputType.member, + type: "IN", + parentId: this.rootName, + mappings: this.context.model.mappings, + portPrefix: QUERY_OUTPUT_TARGET_PORT_PREFIX, + parent: parentPort, + collapsedFields, + expandedFields, + hidden: parentPort.attributes.collapsed + }); + + } + } + + initLinks(): void { + const inputSearch = useDMSearchStore.getState().inputSearch; + const outputSearch = useDMSearchStore.getState().outputSearch; + this.filteredMappings = getFilteredMappings(this.context.model.mappings, inputSearch, outputSearch); + this.createLinks(this.filteredMappings); + } + + private createLinks(mappings: Mapping[]) { + mappings.forEach((mapping) => { + const { isComplex, isQueryExpression, inputs, output, expression, diagnostics } = mapping; + if (isComplex || isQueryExpression || inputs.length !== 1) { + // Complex mappings are handled in the LinkConnectorNode + return; + } + + const inputNode = findInputNode(inputs[0], this); + let inPort: InputOutputPortModel; + if (inputNode) { + inPort = getInputPort(inputNode, inputs[0].replace(/\.\d+/g, '')); + } + + const [_, mappedOutPort] = getOutputPort(this, output); + + if (inPort && mappedOutPort) { + const lm = new DataMapperLinkModel(expression, diagnostics, true, undefined); + lm.setTargetPort(mappedOutPort); + lm.setSourcePort(inPort); + inPort.addLinkedPort(mappedOutPort); + + lm.addLabel( + new ExpressionLabelModel({ + value: expression, + link: lm, + context: this.context, + deleteLink: () => this.deleteField(output), + } + )); + + lm.registerListener({ + selectionChanged(event) { + if (event.isSelected) { + inPort.fireEvent({}, "link-selected"); + mappedOutPort.fireEvent({}, "link-selected"); + } else { + inPort.fireEvent({}, "link-unselected"); + mappedOutPort.fireEvent({}, "link-unselected"); + } + }, + }); + + this.getModel().addAll(lm as any); + } + }); + + const { inputs, output} = this.context.model.query; + + const inputNode = findInputNode(inputs[0], this); + let inPort: InputOutputPortModel; + if (inputNode) { + inPort = getInputPort(inputNode, inputs[0].replace(/\.\d+/g, '')); + } + + const [_, mappedOutPort] = getOutputPort(this, output); + + 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(field: string) { + await removeMapping(field, this.context); + } + + public updatePosition() { + this.setPosition(this.position.x, this.position.y); + } + + setPosition(point: Point): void; + setPosition(x: number, y: number): void; + setPosition(x: unknown, y?: unknown): void { + if (typeof x === 'number' && typeof y === 'number') { + if (!this.x || !this.y) { + this.x = x; + this.y = y; + } + super.setPosition(x, y); + } + } +} diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/QueryOutput/QueryOutputNodeFactory.tsx b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/QueryOutput/QueryOutputNodeFactory.tsx new file mode 100644 index 00000000000..1fcebc0295e --- /dev/null +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/QueryOutput/QueryOutputNodeFactory.tsx @@ -0,0 +1,62 @@ +/** + * 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-lambda jsx-no-multiline-js +import * as React from 'react'; + +import { AbstractReactFactory } from '@projectstorm/react-canvas-core'; +import { DiagramEngine } from '@projectstorm/react-diagrams-core'; + +import { InputOutputPortModel } from '../../Port'; +import { QUERY_OUTPUT_TARGET_PORT_PREFIX } from '../../utils/constants'; +import { QueryOutputWidget } from "./QueryOutputWidget"; +import { OutputSearchNoResultFound, SearchNoResultFoundKind } from "../commons/Search"; + +import { QueryOutputNode, QUERY_OUTPUT_NODE_TYPE } from './QueryOutputNode'; + +export class QueryOutputNodeFactory extends AbstractReactFactory { + constructor() { + super(QUERY_OUTPUT_NODE_TYPE); + } + + generateReactWidget(event: { model: QueryOutputNode; }): JSX.Element { + return ( + <> + {event.model.hasNoMatchingFields ? ( + + ) : ( + event.model.getPort(portId) as InputOutputPortModel} + context={event.model.context} + mappings={event.model.filteredMappings} + valueLabel={event.model.outputType.variableName} + originalTypeName={event.model.filteredOutputType?.variableName} + /> + )} + + ); + } + + generateModel(): QueryOutputNode { + return undefined; + } +} diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/QueryOutput/QueryOutputWidget.tsx b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/QueryOutput/QueryOutputWidget.tsx new file mode 100644 index 00000000000..1ffa090f1d6 --- /dev/null +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/QueryOutput/QueryOutputWidget.tsx @@ -0,0 +1,190 @@ +/** + * 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'; +import { Button, Codicon } from '@wso2/ui-toolkit'; +import { IOType, Mapping } from '@wso2/ballerina-core'; +import { useShallow } from 'zustand/react/shallow'; + +import { IDataMapperContext } from "../../../../utils/DataMapperContext/DataMapperContext"; +import { DataMapperPortWidget, PortState, InputOutputPortModel } from '../../Port'; +import { TreeBody, TreeContainer, TreeHeader } from '../commons/Tree/Tree'; +import { ObjectOutputFieldWidget } from "../ObjectOutput/ObjectOutputFieldWidget"; +import { useIONodesStyles } from '../../../styles'; +import { useDMCollapsedFieldsStore, useDMIOConfigPanelStore } from '../../../../store/store'; +import { OutputSearchHighlight } from '../commons/Search'; +import { OutputBeforeInputNotification } from '../commons/OutputBeforeInputNotification'; + +export interface QueryOutputWidgetProps { + id: string; // this will be the root ID used to prepend for UUIDs of nested fields + outputType: IOType; + typeName: string; + value: any; + engine: DiagramEngine; + getPort: (portId: string) => InputOutputPortModel; + context: IDataMapperContext; + mappings?: Mapping[]; + valueLabel?: string; + originalTypeName?: string; +} + +export function QueryOutputWidget(props: QueryOutputWidgetProps) { + const { + id, + outputType, + typeName, + value, + engine, + getPort, + context, + valueLabel + } = props; + const classes = useIONodesStyles(); + + const [portState, setPortState] = useState(PortState.Unselected); + const [isHovered, setIsHovered] = useState(false); + const [hasOutputBeforeInput, setHasOutputBeforeInput] = useState(false); + + const collapsedFieldsStore = useDMCollapsedFieldsStore(); + + const { setIsIOConfigPanelOpen, setIOConfigPanelType, setIsSchemaOverridden } = useDMIOConfigPanelStore( + useShallow(state => ({ + setIsIOConfigPanelOpen: state.setIsIOConfigPanelOpen, + setIOConfigPanelType: state.setIOConfigPanelType, + setIsSchemaOverridden: state.setIsSchemaOverridden + })) + ); + + const fields = [outputType.member]; + const hasFields = fields.length > 0; + + const portIn = getPort(`${id}.IN`); + + let expanded = true; + if ((portIn && portIn.attributes.collapsed)) { + expanded = false; + } + const isDisabled = portIn?.attributes.descendantHasValue; + + const indentation = (portIn && (!hasFields || !expanded)) ? 0 : 24; + + const handleExpand = () => { + const collapsedFields = collapsedFieldsStore.fields; + if (!expanded) { + collapsedFieldsStore.setFields(collapsedFields.filter((element) => element !== id)); + } else { + collapsedFieldsStore.setFields([...collapsedFields, id]); + } + }; + + const handlePortState = (state: PortState) => { + setPortState(state) + }; + + const handlePortSelection = (outputBeforeInput: boolean) => { + setHasOutputBeforeInput(outputBeforeInput); + }; + + const onMouseEnter = () => { + setIsHovered(true); + }; + + const onMouseLeave = () => { + setIsHovered(false); + }; + + const label = ( + + {valueLabel && ( + + {valueLabel} + {typeName && ":"} + + )} + + {typeName || ''} + + + ); + + const onRightClick = (event: React.MouseEvent) => { + event.preventDefault(); + setIOConfigPanelType("Output"); + setIsSchemaOverridden(true); + setIsIOConfigPanelOpen(true); + }; + + return ( + <> + + + + {portIn && ( + ) + } + + + + {label} + + {hasOutputBeforeInput && } + + {(expanded && fields) && ( + + {fields?.map((item, index) => { + return ( + + ); + })} + + )} + + + ); +} diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/QueryOutput/index.ts b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/QueryOutput/index.ts new file mode 100644 index 00000000000..df282b5658b --- /dev/null +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/QueryOutput/index.ts @@ -0,0 +1,19 @@ +/** + * 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 "./QueryOutputNodeFactory"; +export * from "./QueryOutputNode"; diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/SubMapping/SubMappingItemWidget.tsx b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/SubMapping/SubMappingItemWidget.tsx new file mode 100644 index 00000000000..d735c682ad1 --- /dev/null +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/SubMapping/SubMappingItemWidget.tsx @@ -0,0 +1,253 @@ +/** + * 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 { Button, Codicon, ProgressRing, TruncatedLabel } from "@wso2/ui-toolkit"; +import { DiagramEngine } from '@projectstorm/react-diagrams'; +import classnames from "classnames"; + +import { IDataMapperContext } from "../../../../utils/DataMapperContext/DataMapperContext"; +import { DataMapperPortWidget, PortState, InputOutputPortModel } from '../../Port'; +import { OutputSearchHighlight } from "../commons/Search"; +import { TreeBody } from '../commons/Tree/Tree'; +import { useIONodesStyles } from "../../../styles"; +import { InputNodeTreeItemWidget } from "../Input/InputNodeTreeItemWidget"; +import { useDMExpandedFieldsStore, useDMSubMappingConfigPanelStore } from "../../../../store/store"; +import { DMSubMapping } from "./SubMappingNode"; +import { SubMappingSeparator } from "./SubMappingSeparator"; +import { IOType, TypeKind } from "@wso2/ballerina-core"; +import { getTypeName } from "../../utils/type-utils"; +import { genVariableName, getSubMappingViewLabel } from "../../utils/common-utils"; + +export interface SubMappingItemProps { + index: number; + id: string; // this will be the root ID used to prepend for UUIDs of nested fields + name: string; + type: IOType; + engine: DiagramEngine; + context: IDataMapperContext; + subMappings: DMSubMapping[]; + getPort: (portId: string) => InputOutputPortModel; +}; + +export function SubMappingItemWidget(props: SubMappingItemProps) { + const { index, id, name, type, engine, context, subMappings, getPort } = props; + const { views, addView, applyModifications } = context; + const isOnRootView = views.length === 1; + + const classes = useIONodesStyles(); + const expandedFieldsStore = useDMExpandedFieldsStore(); + const setSubMappingConfig = useDMSubMappingConfigPanelStore(state => state.setSubMappingConfig); + + const [ portState, setPortState ] = useState(PortState.Unselected); + const [isHovered, setIsHovered] = useState(false); + const [deleteInProgress, setDeleteInProgress] = useState(false); + + const typeName = getTypeName(type); + const portOut = getPort(`${id}.OUT`); + const isRecord = type.kind === TypeKind.Record; + const hasFields = !!type?.fields?.length; + const isFirstItem = index === 0; + const isLastItem = index === subMappings.length - 1; + const expanded = !(portOut && portOut.attributes.collapsed); + + const label = ( + + + {name} + + {typeName && ( + + {typeName} + + )} + + + ); + + const onClickAddSubMappingAtTop = () => { + addSubMapping(0); + }; + + const onClickAddSubMapping = () => { + addSubMapping(index + 1); + }; + + const addSubMapping = (targetIndex: number) => { + const varName = genVariableName("subMapping", subMappings.map(mapping => mapping.name)); + setSubMappingConfig({ + isSMConfigPanelOpen: true, + nextSubMappingIndex: targetIndex, + suggestedNextSubMappingName: varName + }); + }; + + const handleExpand = () => { + const expandedFields = expandedFieldsStore.fields; + if (expanded) { + expandedFieldsStore.setFields(expandedFields.filter((element) => element !== id)); + } else { + expandedFieldsStore.setFields([...expandedFields, id]); + } + } + + const onMouseEnter = () => { + setIsHovered(true); + }; + + const onMouseLeave = () => { + setIsHovered(false); + }; + + const handlePortState = (state: PortState) => { + setPortState(state) + }; + + const onClickOnExpand = () => { + const subMapping = subMappings[index]; + const label = getSubMappingViewLabel(subMapping.name, subMapping.type); + + addView( + { + targetField: subMapping.name, + label: label, + subMappingInfo: { + index, + mappingName: subMapping.name, + mappingType: typeName, + focusedOnSubMappingRoot: true + } + } + ); + }; + + const onClickOnDelete = async () => { + setDeleteInProgress(true); + // TODO: Update mappings array and apply modification + setDeleteInProgress(false); + }; + + return ( + <> + {isFirstItem && ( + + )} +
+ + {isRecord && hasFields && ( + + )} + {label} + + {deleteInProgress ? : + + } + + + {portOut && ( + + )} + +
+ { + expanded && isRecord && hasFields && ( + + {type.fields.map((field, index) => { + return ( + + ); + })} + + ) + } + + {isLastItem && isOnRootView && ( + + )} + + ); +} diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/SubMapping/SubMappingNode.ts b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/SubMapping/SubMappingNode.ts new file mode 100644 index 00000000000..e75ee980f52 --- /dev/null +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/SubMapping/SubMappingNode.ts @@ -0,0 +1,142 @@ +/** + * 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 { Point } from "@projectstorm/geometry"; +import { IOType, TypeKind } from "@wso2/ballerina-core"; + +import { IDataMapperContext } from "../../../../utils/DataMapperContext/DataMapperContext"; +import { DataMapperNodeModel } from "../commons/DataMapperNode"; +import { useDMCollapsedFieldsStore, useDMExpandedFieldsStore, useDMSearchStore } from "../../../../store/store"; +import { SUB_MAPPING_INPUT_SOURCE_PORT_PREFIX } from "../../utils/constants"; +import { getSearchFilteredInput } from "../../utils/search-utils"; + +export const SUB_MAPPING_SOURCE_NODE_TYPE = "datamapper-node-sub-mapping"; +const NODE_ID = "sub-mapping-node"; + +export interface DMSubMapping { + name: string; + type: IOType; +} + +export class SubMappingNode extends DataMapperNodeModel { + public hasNoMatchingFields: boolean; + public x: number; + public numberOfFields: number; + + constructor( + public context: IDataMapperContext, + public subMappings: DMSubMapping[] + ) { + super( + NODE_ID, + context, + SUB_MAPPING_SOURCE_NODE_TYPE + ); + this.numberOfFields = 1; + this.subMappings = subMappings; + } + + async initPorts() { + const { views } = this.context; + const searchValue = useDMSearchStore.getState().inputSearch; + const focusedView = views[views.length - 1]; + const subMappingView = focusedView.subMappingInfo; + + this.subMappings.forEach((subMapping, index) => { + // Constraint: Only one variable declaration is allowed in a local variable statement. + + if (subMappingView) { + if (index >= subMappingView.index) { + // Skip the variable declarations that are after the focused sub-mapping + return; + } + } + + const varName = subMapping.name; + const typeWithoutFilter: IOType = subMapping.type; + + const type: IOType = getSearchFilteredInput(typeWithoutFilter, varName); + + if (type) { + const collapsedFields = useDMCollapsedFieldsStore.getState().fields; + const expandedFields = useDMExpandedFieldsStore.getState().fields; + const focusedFieldFQNs = this.context.model.query?.inputs || []; + + const parentPort = this.addPortsForHeader({ + dmType: type, + name: varName, + portType: "OUT", + portPrefix: SUB_MAPPING_INPUT_SOURCE_PORT_PREFIX, + collapsedFields, + expandedFields, + focusedFieldFQNs + }); + + if (type.kind === TypeKind.Record) { + const fields = type.fields; + fields.forEach(subField => { + this.numberOfFields += 1 + this.addPortsForInputField({ + field: subField, + portType: "OUT", + parentId: varName, + unsafeParentId: varName, + portPrefix: SUB_MAPPING_INPUT_SOURCE_PORT_PREFIX, + parent: parentPort, + collapsedFields, + expandedFields, + hidden: parentPort.attributes.collapsed, + isOptional: subField.optional, + focusedFieldFQNs + }); + }); + } else { + this.addPortsForInputField({ + field: type, + portType: "OUT", + parentId: varName, + unsafeParentId: varName, + portPrefix: SUB_MAPPING_INPUT_SOURCE_PORT_PREFIX, + parent: parentPort, + collapsedFields, + expandedFields, + hidden: parentPort.attributes.collapsed, + isOptional: type.optional, + focusedFieldFQNs + }); + } + } + + }); + + this.hasNoMatchingFields = searchValue && this.subMappings.length === 0; + } + + async initLinks() { + // Links are always created from "IN" ports by backtracing the inputs. + } + + setPosition(point: Point): void; + setPosition(x: number, y: number): void; + setPosition(x: unknown, y?: unknown): void { + if (typeof x === 'number' && typeof y === 'number'){ + if (!this.x){ + this.x = x; + } + super.setPosition(this.x, y); + } + } +} diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/SubMapping/SubMappingNodeFactory.tsx b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/SubMapping/SubMappingNodeFactory.tsx new file mode 100644 index 00000000000..7deac7db7fc --- /dev/null +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/SubMapping/SubMappingNodeFactory.tsx @@ -0,0 +1,54 @@ +/** + * 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 { InputOutputPortModel } from '../../Port'; +import { InputSearchNoResultFound, SearchNoResultFoundKind } from "../commons/Search"; + +import { SubMappingNode, SUB_MAPPING_SOURCE_NODE_TYPE } from "./SubMappingNode"; +import { SubMappingTreeWidget } from "./SubMappingTreeWidget"; + +export class SubMappingNodeFactory extends AbstractReactFactory { + constructor() { + super(SUB_MAPPING_SOURCE_NODE_TYPE); + } + + generateReactWidget(event: { model: SubMappingNode; }): JSX.Element { + return ( + <> + {event.model.hasNoMatchingFields ? ( + + ) : ( + event.model.getPort(portId) as InputOutputPortModel} + /> + )} + + ); + } + + generateModel(): SubMappingNode { + return undefined; + } +} diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/SubMapping/SubMappingSeparator.tsx b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/SubMapping/SubMappingSeparator.tsx new file mode 100644 index 00000000000..17af53092ec --- /dev/null +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/SubMapping/SubMappingSeparator.tsx @@ -0,0 +1,90 @@ +/** + * 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 React, { useState } from "react"; + +import { keyframes } from "@emotion/react"; +import styled from "@emotion/styled"; +import { Button, Codicon } from "@wso2/ui-toolkit"; + +import { useIONodesStyles } from "../../../../components/styles"; + +interface SubMappingSeparatorProps { + isOnRootView: boolean; + onClickAddSubMapping: () => void; + isLastItem?: boolean; +}; + +const fadeInZoomIn = keyframes` + 0% { + opacity: 0; + transform: scale(0.5); + } + 100% { + opacity: 1; + transform: scale(1); + } +`; + +const zoomIn = keyframes` + 0% { + transform: scale(0.9); + } + 100% { + transform: scale(1.2); + } +`; + +const HoverButton = styled(Button)` + animation: ${fadeInZoomIn} 0.2s ease-out forwards; + &:hover { + animation: ${zoomIn} 0.2s ease-out forwards; + }; +`; + +export function SubMappingSeparator(props: SubMappingSeparatorProps) { + const { isOnRootView, onClickAddSubMapping, isLastItem } = props; + const classes = useIONodesStyles(); + const [isHoveredSeperator, setIsHoveredSeperator] = useState(false); + + const handleMouseEnter = () => { + setIsHoveredSeperator(true); + }; + + const handleMouseLeave = () => { + setIsHoveredSeperator(false); + }; + + return ( +
+ {isHoveredSeperator && !isLastItem && isOnRootView && ( + + + + )} +
+ ); +}; diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/SubMapping/SubMappingTreeWidget.tsx b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/SubMapping/SubMappingTreeWidget.tsx new file mode 100644 index 00000000000..e8382a72dab --- /dev/null +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/SubMapping/SubMappingTreeWidget.tsx @@ -0,0 +1,114 @@ +/** + * 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 React, { ReactNode } from 'react'; + +import styled from "@emotion/styled"; +import { Button, Codicon, Icon } from '@wso2/ui-toolkit'; +import { DiagramEngine } from '@projectstorm/react-diagrams'; + +import { useDMSearchStore, useDMSubMappingConfigPanelStore } from "../../../../store/store"; +import { IDataMapperContext } from "../../../../utils/DataMapperContext/DataMapperContext"; +import { InputOutputPortModel } from '../../Port'; +import { SUB_MAPPING_INPUT_SOURCE_PORT_PREFIX } from "../../utils/constants"; +import { SharedContainer } from '../commons/Tree/Tree'; +import { DMSubMapping } from "./index"; +import { useIONodesStyles } from '../../../styles'; +import { SubMappingItemWidget } from './SubMappingItemWidget'; + +const SubMappingsHeader = styled.div` + background: var(--vscode-sideBarSectionHeader-background); + height: 40px; + width: 100%; + line-height: 35px; + display: flex; + align-items: center; + justify-content: space-between; + cursor: default; +`; + +const HeaderText = styled.span` + margin-left: 10px; + min-width: 280px; + font-size: 13px; + font-weight: 600; + color: var(--vscode-inputOption-activeForeground) +`; + +export interface SubMappingTreeWidgetProps { + subMappings: DMSubMapping[]; + engine: DiagramEngine; + context: IDataMapperContext; + getPort: (portId: string) => InputOutputPortModel; +} + +export function SubMappingTreeWidget(props: SubMappingTreeWidgetProps) { + const { engine, subMappings, context, getPort } = props; + const searchValue = useDMSearchStore.getState().inputSearch; + const isFocusedView = context.views.length > 1; + + const classes = useIONodesStyles(); + const setSubMappingConfig = useDMSubMappingConfigPanelStore(state => state.setSubMappingConfig); + + const subMappingItems: ReactNode[] = subMappings.map((mapping, index) => { + return ( + getPort(portId) as InputOutputPortModel} + /> + ); + }).filter(mapping => !!mapping); + + const onClickAddSubMapping = () => { + setSubMappingConfig({ + isSMConfigPanelOpen: true, + nextSubMappingIndex: 0, + suggestedNextSubMappingName: "subMapping" + }); + }; + + return ( + <> + {subMappingItems.length > 0 ? ( + + + Sub Mappings + + {subMappingItems} + + ) : !isFocusedView && !searchValue && ( + + )} + + ); +} diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/SubMapping/index.ts b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/SubMapping/index.ts new file mode 100644 index 00000000000..0b105136161 --- /dev/null +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/SubMapping/index.ts @@ -0,0 +1,19 @@ +/** + * 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 "./SubMappingNode"; +export * from "./SubMappingNodeFactory"; diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/commons/DataMapperNode.ts b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/commons/DataMapperNode.ts index 283f06c0866..2883a42068b 100644 --- a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/commons/DataMapperNode.ts +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/commons/DataMapperNode.ts @@ -22,12 +22,53 @@ import { IOType, Mapping, MappingElement, TypeKind } from '@wso2/ballerina-core' import { IDataMapperContext } from '../../../../utils/DataMapperContext/DataMapperContext'; import { MappingMetadata } from '../../Mappings/MappingMetadata'; import { InputOutputPortModel } from "../../Port"; -import { findMappingByOutput, isCollapsed } from '../../utils/common-utils'; +import { findMappingByOutput } from '../../utils/common-utils'; export interface DataMapperNodeModelGenerics { PORT: InputOutputPortModel; } +interface InputPortAttributes { + field: IOType; + portType: "IN" | "OUT"; + parentId: string; + unsafeParentId: string; + portPrefix?: string; + parent?: InputOutputPortModel; + collapsedFields?: string[]; + expandedFields?: string[]; + hidden?: boolean; + collapsed?: boolean; + isOptional?: boolean; + focusedFieldFQNs?: string[]; +}; + +interface OutputPortAttributes { + field: IOType; + type: "IN" | "OUT"; + parentId: string; + mappings: Mapping[]; + portPrefix?: string; + parent?: InputOutputPortModel; + collapsedFields?: string[]; + expandedFields?: string[]; + hidden?: boolean; + elementIndex?: number; + isPreview?: boolean; +}; + +interface HeaderPortAttributes { + dmType: IOType; + name: string; + portType: "IN" | "OUT"; + portPrefix?: string; + mappings?: Mapping[]; + collapsedFields?: string[]; + expandedFields?: string[]; + isPreview?: boolean; + focusedFieldFQNs?: string[]; +}; + export abstract class DataMapperNodeModel extends NodeModel { private diagramModel: DiagramModel; @@ -54,107 +95,111 @@ export abstract class DataMapperNodeModel extends NodeModel
{typeName && ( - + {typeName} )} diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/commons/Search/SearchNoResultFound.tsx b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/commons/Search/SearchNoResultFound.tsx index 3643f5d5629..38b2b352916 100644 --- a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/commons/Search/SearchNoResultFound.tsx +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/commons/Search/SearchNoResultFound.tsx @@ -1,28 +1,23 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. +/* + * Copyright (c) 2024, WSO2 LLC. (http://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. + * This software is the property of WSO2 LLC. and its suppliers, if any. + * Dissemination of any information or reproduction of any material contained + * herein is strictly forbidden, unless permitted by WSO2 in accordance with + * the WSO2 Commercial License available at http://wso2.com/licenses. + * For specific language governing the permissions and limitations under + * this license, please see the license as well as any agreement you’ve + * entered into with WSO2 governing the purchase of this software and any + * associated services. */ // tslint:disable: jsx-no-multiline-js import React from "react"; import { css } from "@emotion/css"; - -import { TreeContainer } from "../Tree/Tree"; import { Codicon } from "@wso2/ui-toolkit"; +import { IO_NODE_DEFAULT_WIDTH } from "../../../../../components/Diagram/utils/constants"; + interface SearchNoResultFoundProps { kind: SearchNoResultFoundKind; } @@ -36,24 +31,33 @@ export enum SearchNoResultFoundKind { } const useStyles = () => ({ + treeContainer: css({ + width: `${IO_NODE_DEFAULT_WIDTH}px`, + cursor: "default", + padding: "12px", + fontFamily: "GilmerRegular", + background: "var(--vscode-sideBar-background)", + border: "1.8px dashed var(--vscode-dropdown-border)", + borderRadius: "6px" + }), noResultFoundBanner: css({ width: "320px", - padding: "10px", display: "flex", + opacity: 0.8 }) }); function SearchNoResultFound({ kind }: SearchNoResultFoundProps) { const classes = useStyles(); return ( - +
- +
{`No matching ${kind} found`}
- +
); } diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/commons/Search/index.tsx b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/commons/Search/index.tsx index 937b371087e..891f0f959fb 100644 --- a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/commons/Search/index.tsx +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/commons/Search/index.tsx @@ -1,19 +1,14 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. +/* + * Copyright (c) 2024, WSO2 LLC. (http://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. + * This software is the property of WSO2 LLC. and its suppliers, if any. + * Dissemination of any information or reproduction of any material contained + * herein is strictly forbidden, unless permitted by WSO2 in accordance with + * the WSO2 Commercial License available at http://wso2.com/licenses. + * For specific language governing the permissions and limitations under + * this license, please see the license as well as any agreement you’ve + * entered into with WSO2 governing the purchase of this software and any + * associated services. */ export { InputSearchHighlight, diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/commons/Tree/Tree.tsx b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/commons/Tree/Tree.tsx index e8cabfec7bb..0a0738847bc 100644 --- a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/commons/Tree/Tree.tsx +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/commons/Tree/Tree.tsx @@ -29,12 +29,13 @@ export const TreeContainer = styled.div` align-items: flex-start; gap: ${GAP_BETWEEN_NODE_HEADER_AND_BODY}px; background: var(--vscode-sideBar-background); - border: 1px solid var(--vscode-welcomePage-tileBorder); + border: 1.8px solid var(--vscode-dropdown-border); font-style: normal; font-weight: 600; font-size: 13px; line-height: 24px; width: ${IO_NODE_DEFAULT_WIDTH}px; + border-radius: 6px; `; export const SharedContainer = styled.div` @@ -42,7 +43,7 @@ export const SharedContainer = styled.div` flex-direction: column; align-items: flex-start; background: var(--vscode-sideBar-background); - border: 1px solid var(--vscode-welcomePage-tileBorder); + border: 1.8px solid var(--vscode-dropdown-border); font-style: normal; font-weight: 600; font-size: 13px; @@ -66,9 +67,8 @@ export const TreeHeader = styled.div<{ isSelected?: boolean; isDisabled?: boolea : 'var(--vscode-list-hoverBackground)', }, color: 'var(--vscode-inputOption-activeForeground)', - }) -); - + borderBottom: '1.8px solid var(--vscode-dropdown-border)' +})); export const TreeBody = styled.div` display: flex; diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/commons/ValueConfigButton/ValueConfigMenu.tsx b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/commons/ValueConfigButton/ValueConfigMenu.tsx index 042b501bc4a..b653c8ec466 100644 --- a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/commons/ValueConfigButton/ValueConfigMenu.tsx +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/commons/ValueConfigButton/ValueConfigMenu.tsx @@ -34,6 +34,7 @@ export enum ValueConfigOption { InitializeWithValue = "Initialize With Default Value", EditValue = "Edit Value", InitializeArray = "Initialize Array", + AddElement = "Add Element", DeleteValue = "Delete Value", DeleteElement = "Delete Element", DeleteArray = "Delete Array", diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/index.ts b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/index.ts index 12311e4b675..8da41fe9613 100644 --- a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/index.ts +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/index.ts @@ -18,7 +18,11 @@ export * from "./DataImport"; export * from "./Input"; +export * from "./SubMapping"; export * from "./ObjectOutput"; export * from "./ArrayOutput"; +export * from "./PrimitiveOutput"; +export * from "./QueryOutput"; export * from "./LinkConnector"; +export * from "./QueryExprConnector"; export * from "./EmptyInputs"; diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Port/model/InputOutputPortModel.ts b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Port/model/InputOutputPortModel.ts index 40b92653849..16948b6de39 100644 --- a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Port/model/InputOutputPortModel.ts +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Port/model/InputOutputPortModel.ts @@ -20,9 +20,8 @@ import { IOType, Mapping } from "@wso2/ballerina-core"; import { DataMapperLinkModel } from "../../Link"; import { IntermediatePortModel } from "../IntermediatePort"; -import { createNewMapping, updateExistingMapping } from "../../utils/modification-utils"; +import { createNewMapping } from "../../utils/modification-utils"; import { getMappingType } from "../../utils/common-utils"; -import { getValueType } from "../../utils/common-utils"; export interface InputOutputPortModelGenerics { PORT: InputOutputPortModel; @@ -42,29 +41,33 @@ export enum MappingType { Default = undefined // This is for non-array mappings currently } +export interface PortAttributes { + field: IOType; + portName: string; + portType: "IN" | "OUT"; + value?: Mapping; + index?: number; + fieldFQN?: string; // Field FQN with optional included, ie. person?.name?.firstName + optionalOmittedFieldFQN?: string; // Field FQN without optional, ie. person.name.firstName + parentModel?: InputOutputPortModel; + collapsed?: boolean; + hidden?: boolean; + descendantHasValue?: boolean; + ancestorHasValue?: boolean; + isPreview?: boolean; +}; + export class InputOutputPortModel extends PortModel { public linkedPorts: PortModel[]; + public attributes: PortAttributes; - constructor( - public field: IOType, - public portName: string, - public portType: "IN" | "OUT", - public value?: Mapping, - public index?: number, - public fieldFQN?: string, // Field FQN with optional included, ie. person?.name?.firstName - public optionalOmittedFieldFQN?: string, // Field FQN without optional, ie. person.name.firstName - public parentModel?: InputOutputPortModel, - public collapsed?: boolean, - public hidden?: boolean, - public descendantHasValue?: boolean, - public ancestorHasValue?: boolean, - public isWithinMapFunction?: boolean, - ) { + constructor(public portAttributes: PortAttributes) { super({ type: INPUT_OUTPUT_PORT, - name: `${portName}.${portType}` + name: `${portAttributes.portName}.${portAttributes.portType}` }); + this.attributes = portAttributes; this.linkedPorts = []; } @@ -78,18 +81,11 @@ export class InputOutputPortModel extends PortModel link instanceof DataMapperLinkModel && link.isActualLink); - const valueType = getValueType(lm); - - if (targetPortHasLinks || valueType === ValueType.NonEmpty) { - await updateExistingMapping(lm); - } else { - await createNewMapping(lm); - } + await createNewMapping(lm); }) }); @@ -97,8 +93,8 @@ export class InputOutputPortModel extends PortModel): void { - if (this.portType === 'IN'){ - this.parentModel?.setDescendantHasValue(); + if (this.attributes.portType === 'IN'){ + this.attributes.parentModel?.setDescendantHasValue(); } super.addLink(link); } @@ -108,24 +104,24 @@ export class InputOutputPortModel extends PortModel { return port.getID() === linkedPort.getID() }) } - return this.portType !== port.portType && !isLinkExists + return this.attributes.portType !== port.attributes.portType && !isLinkExists && ((port instanceof IntermediatePortModel) || (!port.isDisabled())); } } diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/utils/common-utils.ts b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/utils/common-utils.ts index 49b80599c32..0f6db786669 100644 --- a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/utils/common-utils.ts +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/utils/common-utils.ts @@ -18,13 +18,21 @@ import { PortModel } from "@projectstorm/react-diagrams-core"; import { InputOutputPortModel, MappingType, ValueType } from "../Port"; -import { getDMTypeDim } from "./type-utils"; -import { DataMapperLinkModel } from "../Link"; +import { getDMTypeDim, getTypeName } from "./type-utils"; import { IOType, Mapping, TypeKind } from "@wso2/ballerina-core"; -import { useDMCollapsedFieldsStore, useDMExpandedFieldsStore } from "../../../store/store"; import { DataMapperNodeModel } from "../Node/commons/DataMapperNode"; import { ErrorNodeKind } from "../../../components/DataMapper/Error/RenderingError"; -import { ARRAY_OUTPUT_NODE_TYPE, INPUT_NODE_TYPE, OBJECT_OUTPUT_NODE_TYPE } from "../Node"; +import { + ARRAY_OUTPUT_NODE_TYPE, + EmptyInputsNode, + INPUT_NODE_TYPE, + InputNode, + OBJECT_OUTPUT_NODE_TYPE, + PRIMITIVE_OUTPUT_NODE_TYPE +} from "../Node"; +import { IDataMapperContext } from "../../../utils/DataMapperContext/DataMapperContext"; +import { View } from "../../../components/DataMapper/Views/DataMapperView"; +import { useDMExpandedFieldsStore } from "../../../store/store"; export function findMappingByOutput(mappings: Mapping[], outputId: string): Mapping { return mappings.find(mapping => (mapping.output === outputId || mapping.output.replaceAll("\"", "") === outputId)); @@ -34,10 +42,10 @@ export function getMappingType(sourcePort: PortModel, targetPort: PortModel): Ma if (sourcePort instanceof InputOutputPortModel && targetPort instanceof InputOutputPortModel - && targetPort.field && sourcePort.field) { + && targetPort.attributes.field && sourcePort.attributes.field) { - const sourceDim = getDMTypeDim(sourcePort.field); - const targetDim = getDMTypeDim(targetPort.field); + const sourceDim = getDMTypeDim(sourcePort.attributes.field); + const targetDim = getDMTypeDim(targetPort.attributes.field); if (sourceDim > 0) { const dimDelta = sourceDim - targetDim; @@ -52,8 +60,8 @@ export function getMappingType(sourcePort: PortModel, targetPort: PortModel): Ma export function genArrayElementAccessSuffix(sourcePort: PortModel, targetPort: PortModel) { if (sourcePort instanceof InputOutputPortModel && targetPort instanceof InputOutputPortModel) { let suffix = ''; - const sourceDim = getDMTypeDim(sourcePort.field); - const targetDim = getDMTypeDim(targetPort.field); + const sourceDim = getDMTypeDim(sourcePort.attributes.field); + const targetDim = getDMTypeDim(targetPort.attributes.field); const dimDelta = sourceDim - targetDim; for (let i = 0; i < dimDelta; i++) { suffix += '[0]'; @@ -63,16 +71,6 @@ export function genArrayElementAccessSuffix(sourcePort: PortModel, targetPort: P return ''; }; -export function getValueType(lm: DataMapperLinkModel): ValueType { - const { field, value } = lm.getTargetPort() as InputOutputPortModel; - - if (value !== undefined) { - return isDefaultValue(field, value.expression) ? ValueType.Default : ValueType.NonEmpty; - } - - return ValueType.Empty; -} - export function isDefaultValue(field: IOType, value: string): boolean { const defaultValue = getDefaultValue(field.kind); const targetValue = value?.trim().replace(/(\r\n|\n|\r|\s)/g, "") @@ -107,15 +105,6 @@ export function getDefaultValue(typeKind: TypeKind): string { return draftParameter; } -export function isCollapsed(portName: string, portType: "IN" | "OUT"): boolean { - const collapsedFields = useDMCollapsedFieldsStore.getState().fields; - const expandedFields = useDMExpandedFieldsStore.getState().fields; - - // In Inline Data Mapper, the inputs are always collapsed by default. - // Hence we explicitly check expandedFields for input header ports. - return portType === "IN" ? collapsedFields.includes(portName) : !expandedFields.includes(portName); -} - export function fieldFQNFromPortName(portName: string): string { return portName.split('.').slice(1).join('.'); } @@ -125,6 +114,7 @@ export function getErrorKind(node: DataMapperNodeModel): ErrorNodeKind { switch (nodeType) { case OBJECT_OUTPUT_NODE_TYPE: case ARRAY_OUTPUT_NODE_TYPE: + case PRIMITIVE_OUTPUT_NODE_TYPE: return ErrorNodeKind.Output; case INPUT_NODE_TYPE: return ErrorNodeKind.Input; @@ -132,3 +122,58 @@ export function getErrorKind(node: DataMapperNodeModel): ErrorNodeKind { return ErrorNodeKind.Other; } } + +export function expandArrayFn(context: IDataMapperContext, targetField: string){ + + const { addView } = context; + + const newView: View = { + label: targetField, + targetField: targetField + }; + + addView(newView); +} + +export function genVariableName(originalName: string, existingNames: string[]): string { + let modifiedName: string = originalName; + let index = 0; + while (existingNames.includes(modifiedName)) { + index++; + modifiedName = originalName + index; + } + return modifiedName; +} + +export function getSubMappingViewLabel(subMappingName: string, subMappingType: IOType): string { + let label = subMappingName; + if (subMappingType.kind === TypeKind.Array) { + const typeName = getTypeName(subMappingType); + const bracketsCount = (typeName.match(/\[\]/g) || []).length; // Count the number of pairs of brackets + label = label + `${"[]".repeat(bracketsCount)}`; + } + + return label; +} + +export function excludeEmptyInputNodes(nodes: DataMapperNodeModel[]): DataMapperNodeModel[] { + const filtered = nodes.filter(node => + !(node instanceof InputNode) || + node instanceof InputNode && node.getSearchFilteredType() !== undefined + ); + const hasInputNode = filtered.some(node => node instanceof InputNode || node instanceof EmptyInputsNode); + if (!hasInputNode) { + const inputNode = new InputNode(undefined, undefined, true); + filtered.unshift(inputNode); + } + return filtered; +} + +export function handleExpand(id: string, expanded: boolean) { + const expandedFields = useDMExpandedFieldsStore.getState().fields; + if (expanded) { + useDMExpandedFieldsStore.getState().setFields(expandedFields.filter((element) => element !== id)); + } else { + useDMExpandedFieldsStore.getState().setFields([...expandedFields, id]); + } +} diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/utils/constants.ts b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/utils/constants.ts index b38d0c88167..e6e3124516a 100644 --- a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/utils/constants.ts +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/utils/constants.ts @@ -17,6 +17,9 @@ */ export const OBJECT_OUTPUT_TARGET_PORT_PREFIX = "objectOutput"; export const ARRAY_OUTPUT_TARGET_PORT_PREFIX = "arrayOutput"; +export const PRIMITIVE_OUTPUT_TARGET_PORT_PREFIX = "primitiveOutput"; +export const QUERY_OUTPUT_TARGET_PORT_PREFIX = "queryOutput"; +export const SUB_MAPPING_INPUT_SOURCE_PORT_PREFIX = "subMappingInput"; export const defaultModelOptions = { zoom: 90 }; export const VISUALIZER_PADDING = 0; @@ -26,7 +29,7 @@ export const IO_NODE_HEADER_HEIGHT = 40; export const IO_NODE_FIELD_HEIGHT = 35; export const GAP_BETWEEN_INPUT_NODES = 10; export const GAP_BETWEEN_FILTER_NODE_AND_INPUT_NODE = 50; -export const GAP_BETWEEN_NODE_HEADER_AND_BODY = 10; +export const GAP_BETWEEN_NODE_HEADER_AND_BODY = 4; export const GAP_BETWEEN_FIELDS = 1; export const ISSUES_URL = "https://github.com/wso2/product-ballerina-integrator/issues"; @@ -49,5 +52,11 @@ export const OFFSETS = { LINK_CONNECTOR_NODE_WITH_ERROR: { X: 718 }, + QUERY_EXPR_CONNECTOR_NODE: { + X: 750 + }, + QUERY_EXPR_CONNECTOR_NODE_WITH_ERROR: { + X: 718 + }, INTERMEDIATE_CLAUSE_HEIGHT: 80 } diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/utils/diagram-utils.ts b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/utils/diagram-utils.ts index 701698c6f0d..ae3dfe7fb90 100644 --- a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/utils/diagram-utils.ts +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/utils/diagram-utils.ts @@ -20,9 +20,23 @@ import { GAP_BETWEEN_NODE_HEADER_AND_BODY, IO_NODE_FIELD_HEIGHT, IO_NODE_HEADER_HEIGHT, + OFFSETS, defaultModelOptions } from "./constants"; +export function calculateZoomLevel(screenWidth: number) { + const minWidth = 200; + const maxWidth = 850; // After this width, the max zoom level is reached + const minZoom = 20; + const maxZoom = defaultModelOptions.zoom; + + // Ensure the max zoom level is not exceeded + const boundedScreenWidth = Math.min(screenWidth, maxWidth); + const normalizedWidth = (boundedScreenWidth - minWidth) / (maxWidth - minWidth); + const zoomLevel = minZoom + normalizedWidth * (maxZoom - minZoom); + return Math.max(minZoom, Math.min(maxZoom, zoomLevel)); +} + export function getIONodeHeight(noOfFields: number) { return noOfFields * IO_NODE_FIELD_HEIGHT + (IO_NODE_HEADER_HEIGHT - IO_NODE_FIELD_HEIGHT) @@ -33,8 +47,8 @@ export function getIONodeHeight(noOfFields: number) { export function calculateControlPointOffset(screenWidth: number) { const minWidth = 850; const maxWidth = 1500; - const minOffset = 5; - const maxOffset = 30; + const minOffset = 15; + const maxOffset = 90; const clampedWidth = Math.min(Math.max(screenWidth, minWidth), maxWidth); const interpolationFactor = (clampedWidth - minWidth) / (maxWidth - minWidth); diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/utils/link-utils.ts b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/utils/link-utils.ts index b61b9dac90a..02fcdb48404 100644 --- a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/utils/link-utils.ts +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/utils/link-utils.ts @@ -22,7 +22,7 @@ import { InputOutputPortModel } from "../Port"; export function isSourcePortArray(port: PortModel): boolean { if (port instanceof InputOutputPortModel) { - const field = port.field; + const field = port.attributes.field; return field.kind === TypeKind.Array; } return false; @@ -30,7 +30,7 @@ export function isSourcePortArray(port: PortModel): boolean { export function isTargetPortArray(port: PortModel): boolean { if (port instanceof InputOutputPortModel) { - const field = port.field; + const field = port.attributes.field; return field.kind === TypeKind.Array; } return false; diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/utils/modification-utils.ts b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/utils/modification-utils.ts index 07782587b97..ba2f49f39b3 100644 --- a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/utils/modification-utils.ts +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/utils/modification-utils.ts @@ -15,100 +15,53 @@ * specific language governing permissions and limitations * under the License. */ -import { IDMModel, Mapping } from "@wso2/ballerina-core"; import { DataMapperLinkModel } from "../Link"; import { DataMapperNodeModel } from "../Node/commons/DataMapperNode"; import { InputOutputPortModel } from "../Port"; -import { IDataMapperContext } from "src/utils/DataMapperContext/DataMapperContext"; +import { IDataMapperContext } from "../../../utils/DataMapperContext/DataMapperContext"; import { MappingFindingVisitor } from "../../../visitors/MappingFindingVisitor"; import { traverseNode } from "../../../utils/model-utils"; import { MappingDeletionVisitor } from "../../../visitors/MappingDeletionVisitor"; export async function createNewMapping(link: DataMapperLinkModel) { + const sourcePort = link.getSourcePort(); const targetPort = link.getTargetPort(); - if (!targetPort) { + if (!sourcePort || !targetPort) { return; } + const sourcePortModel = sourcePort as InputOutputPortModel; const outputPortModel = targetPort as InputOutputPortModel; + const targetNode = outputPortModel.getNode() as DataMapperNodeModel; - const { mappings } = targetNode.context.model; - const input = (link.getSourcePort() as InputOutputPortModel).optionalOmittedFieldFQN; - const outputPortParts = outputPortModel.portName.split('.'); - const isWithinArray = outputPortParts.some(part => !isNaN(Number(part))); - const model = targetNode.context.model; - - if (isWithinArray) { - createNewMappingWithinArray(outputPortParts.slice(1), input, model); - const updatedMappings = mappings; - return await targetNode.context.applyModifications(updatedMappings); - } else { - const mappingFindingVisitor = new MappingFindingVisitor(outputPortParts.slice(1).join('.')); - traverseNode(model, mappingFindingVisitor); - const targetMapping = mappingFindingVisitor.getTargetMapping(); - - if (targetMapping) { - // Update the existing mapping with the new input - targetMapping.expression = input; - targetMapping.inputs.push(input); - } else { - const newMapping = { - output: outputPortParts.slice(1).join('.'), - inputs: [input], - expression: input - }; - - mappings.push(newMapping); - } - } - return await targetNode.context.applyModifications(mappings); -} + const input = sourcePortModel.attributes.optionalOmittedFieldFQN; + const outputId = outputPortModel.attributes.fieldFQN; + const lastView = targetNode.context.views[targetNode.context.views.length - 1]; + const viewId = lastView?.targetField || null; + const name = targetNode.context.views[0]?.targetField; -export async function updateExistingMapping(link: DataMapperLinkModel) { - const targetPort = link.getTargetPort(); - if (!targetPort) { - return; - } + const { model, applyModifications } = targetNode.context; - const outputPortModel = targetPort as InputOutputPortModel; - const targetNode = outputPortModel.getNode() as DataMapperNodeModel; - const { model } = targetNode.context; - const input = (link.getSourcePort() as InputOutputPortModel).optionalOmittedFieldFQN; - const outputPortParts = outputPortModel.portName.split('.'); - const targetId = outputPortParts.slice(1).join('.'); - - const mappingFindingVisitor = new MappingFindingVisitor(targetId); + const mappingFindingVisitor = new MappingFindingVisitor(outputId); traverseNode(model, mappingFindingVisitor); const targetMapping = mappingFindingVisitor.getTargetMapping(); + let expression = input; + if (targetMapping) { - targetMapping.inputs.push(input); - targetMapping.expression = `${targetMapping.expression} + ${input}`; + expression = `${targetMapping.expression} + ${input}`; } - return await targetNode.context.applyModifications(model.mappings); + return await applyModifications(outputId, expression, viewId, name); } export async function addValue(fieldId: string, value: string, context: IDataMapperContext) { - const { mappings } = context.model; - const isWithinArray = fieldId.split('.').some(part => !isNaN(Number(part))); - - if (isWithinArray) { - createNewMappingWithinArray(fieldId.split('.'), value, context.model); - const updatedMappings = mappings; - return await context.applyModifications(updatedMappings); - } else { - const newMapping: Mapping = { - output: fieldId, - inputs: [], - expression: value - }; - - mappings.push(newMapping); - } + const lastView = context.views[context.views.length - 1]; + const viewId = lastView?.targetField || null; + const name = context.views[0]?.targetField; - return await context.applyModifications(mappings); + return await context.applyModifications(fieldId, value, viewId, name); } export async function removeMapping(fieldId: string, context: IDataMapperContext) { @@ -116,7 +69,8 @@ export async function removeMapping(fieldId: string, context: IDataMapperContext traverseNode(context.model, deletionVisitor); const remainingMappings = deletionVisitor.getRemainingMappings(); - return await context.applyModifications(remainingMappings); + // TODO: Update this once the mapping deletion API is available + return await context.applyModifications("", "", "", ""); } export function buildInputAccessExpr(fieldFqn: string): string { @@ -135,42 +89,3 @@ export function buildInputAccessExpr(fieldFqn: string): string { return result.replace(/(?= 1; i--) { - const targetId = outputPortParts.slice(0, i).join('.'); - - const mappingFindingVisitor = new MappingFindingVisitor(targetId); - traverseNode(model, mappingFindingVisitor); - const targetMapping = mappingFindingVisitor.getTargetMapping(); - - if (targetMapping) { - const arrayIndex = Number(outputPortParts[i]); - const arrayElement = targetMapping.elements.length > 0 ? targetMapping.elements[arrayIndex] : undefined; - - if (arrayElement) { - arrayElement.mappings.push({ - output: outputPortParts.join('.'), - inputs: [input], - expression: input, - elements: [] - }); - } else if (isNaN(arrayIndex)) { - // When mapped directly to an array element - targetMapping.expression = input; - targetMapping.inputs.push(input); - } else { - const newMapping: Mapping = { - output: targetId, - inputs: [input], - expression: input, - elements: [] - }; - targetMapping.elements.push({ - mappings: [newMapping] - }); - } - break; - } - } -} diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/utils/port-utils.ts b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/utils/port-utils.ts index da87fd93764..12b4f52e881 100644 --- a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/utils/port-utils.ts +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/utils/port-utils.ts @@ -17,23 +17,24 @@ */ import { NodeModel } from "@projectstorm/react-diagrams"; -import { InputNode, ObjectOutputNode } from "../Node"; +import { InputNode, ObjectOutputNode, QueryOutputNode } from "../Node"; import { InputOutputPortModel } from "../Port"; -import { ARRAY_OUTPUT_TARGET_PORT_PREFIX, OBJECT_OUTPUT_TARGET_PORT_PREFIX } from "./constants"; +import { ARRAY_OUTPUT_TARGET_PORT_PREFIX, OBJECT_OUTPUT_TARGET_PORT_PREFIX, PRIMITIVE_OUTPUT_TARGET_PORT_PREFIX, QUERY_OUTPUT_TARGET_PORT_PREFIX } from "./constants"; import { ArrayOutputNode } from "../Node/ArrayOutput/ArrayOutputNode"; +import { PrimitiveOutputNode } from "../Node/PrimitiveOutput/PrimitiveOutputNode"; export function getInputPort(node: InputNode, inputField: string): InputOutputPortModel { let port = node.getPort(`${inputField}.OUT`) as InputOutputPortModel; - while (port && port.hidden) { - port = port.parentModel; + while (port && port.attributes.hidden) { + port = port.attributes.parentModel; } return port; } export function getOutputPort( - node: ObjectOutputNode | ArrayOutputNode, + node: ObjectOutputNode | ArrayOutputNode | PrimitiveOutputNode | QueryOutputNode, outputField: string ): [InputOutputPortModel, InputOutputPortModel] { const portId = `${getTargetPortPrefix(node)}.${outputField}.IN`; @@ -43,8 +44,8 @@ export function getOutputPort( const actualPort = port as InputOutputPortModel; let mappedPort = actualPort; - while (mappedPort && mappedPort.hidden) { - mappedPort = mappedPort.parentModel; + while (mappedPort && mappedPort.attributes.hidden) { + mappedPort = mappedPort.attributes.parentModel; } return [actualPort, mappedPort]; @@ -59,7 +60,10 @@ export function getTargetPortPrefix(node: NodeModel): string { return OBJECT_OUTPUT_TARGET_PORT_PREFIX; case node instanceof ArrayOutputNode: return ARRAY_OUTPUT_TARGET_PORT_PREFIX; - // TODO: Update cases for other node types + case node instanceof PrimitiveOutputNode: + return PRIMITIVE_OUTPUT_TARGET_PORT_PREFIX; + case node instanceof QueryOutputNode: + return QUERY_OUTPUT_TARGET_PORT_PREFIX; default: return ""; } diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/utils/search-utils.ts b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/utils/search-utils.ts index 2deccb955cb..06c9b33de57 100644 --- a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/utils/search-utils.ts +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/utils/search-utils.ts @@ -126,18 +126,27 @@ export function hasNoOutputMatchFound(outputType: IOType, filteredOutputType: IO return false; } -export function getFilteredMappings(mappings: Mapping[], searchValue: string): Mapping[] { +export function getFilteredMappings(mappings: Mapping[], inputSearch: string, outputSearch: string): Mapping[] { return mappings.flatMap(mapping => { + + const filteredInputs = mapping.inputs.filter(input => { + const inputField = input.split(".").pop(); + return inputSearch === "" || + inputField.toLowerCase().includes(inputSearch.toLowerCase()); + }); + const outputField = mapping.output.split(".").pop(); - const isCurrentMappingMatched = searchValue === "" || - outputField.toLowerCase().includes(searchValue.toLowerCase()); + const matchedWithOutputSearch = outputSearch === "" || + outputField.toLowerCase().includes(outputSearch.toLowerCase()); // Get nested mappings from elements const nestedMappings = mapping.elements?.flatMap(element => - getFilteredMappings(element.mappings, searchValue) + getFilteredMappings(element.mappings, inputSearch, outputSearch) ) || []; + + const filteredMapping = filteredInputs.length > 0 && matchedWithOutputSearch; // Return current mapping if matched, along with any nested matches - return isCurrentMappingMatched ? [mapping, ...nestedMappings] : nestedMappings; + return filteredMapping ? [mapping, ...nestedMappings] : nestedMappings; }); } diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Hooks/index.tsx b/workspaces/ballerina/inline-data-mapper/src/components/Hooks/index.tsx index df74fbf002c..de2607d9a03 100644 --- a/workspaces/ballerina/inline-data-mapper/src/components/Hooks/index.tsx +++ b/workspaces/ballerina/inline-data-mapper/src/components/Hooks/index.tsx @@ -27,17 +27,27 @@ import { getIONodeHeight } from '../Diagram/utils/diagram-utils'; import { OverlayLayerModel } from '../Diagram/OverlayLayer/OverlayLayerModel'; import { ErrorNodeKind } from '../DataMapper/Error/RenderingError'; import { useDMCollapsedFieldsStore, useDMExpandedFieldsStore, useDMSearchStore } from '../../store/store'; -import { ArrayOutputNode, EmptyInputsNode, InputNode, ObjectOutputNode } from '../Diagram/Node'; -import { GAP_BETWEEN_INPUT_NODES, OFFSETS } from '../Diagram/utils/constants'; +import { + ArrayOutputNode, + EmptyInputsNode, + InputNode, + LinkConnectorNode, + ObjectOutputNode, + PrimitiveOutputNode, + QueryExprConnectorNode, + QueryOutputNode, + SubMappingNode +} from '../Diagram/Node'; +import { GAP_BETWEEN_INPUT_NODES, IO_NODE_DEFAULT_WIDTH, OFFSETS } from '../Diagram/utils/constants'; import { InputDataImportNodeModel, OutputDataImportNodeModel } from '../Diagram/Node/DataImport/DataImportNode'; -import { getErrorKind } from '../Diagram/utils/common-utils'; +import { excludeEmptyInputNodes, getErrorKind } from '../Diagram/utils/common-utils'; export const useRepositionedNodes = ( nodes: DataMapperNodeModel[], zoomLevel: number, diagramModel: DiagramModel ) => { - const nodesClone = [...nodes]; + const nodesClone = [...excludeEmptyInputNodes(nodes)]; const prevNodes = diagramModel.getNodes() as DataMapperNodeModel[]; const filtersUnchanged = false; @@ -48,14 +58,17 @@ export const useRepositionedNodes = ( if (node instanceof ObjectOutputNode || node instanceof ArrayOutputNode + || node instanceof PrimitiveOutputNode + || node instanceof QueryOutputNode || node instanceof OutputDataImportNodeModel ) { - const x = OFFSETS.TARGET_NODE.X; + const x = (window.innerWidth) * (100 / zoomLevel) - IO_NODE_DEFAULT_WIDTH; const y = exisitingNode && exisitingNode.getY() !== 0 ? exisitingNode.getY() : 0; node.setPosition(x, y); } if (node instanceof InputNode || node instanceof EmptyInputsNode + || node instanceof SubMappingNode || node instanceof InputDataImportNodeModel ) { const x = OFFSETS.SOURCE_NODE.X; @@ -88,6 +101,7 @@ export const useDiagramModel = ( const noOfNodes = nodes.length; const context = nodes.find(node => node.context)?.context; const { model } = context ?? {}; + const mappings = model.mappings.map(mapping => mapping.expression).toString(); const collapsedFields = useDMCollapsedFieldsStore(state => state.fields); // Subscribe to collapsedFields const expandedFields = useDMExpandedFieldsStore(state => state.fields); // Subscribe to expandedFields const { inputSearch, outputSearch } = useDMSearchStore(); @@ -102,11 +116,20 @@ export const useDiagramModel = ( const newModel = new DiagramModel(); newModel.setZoomLevel(zoomLevel); newModel.setOffset(offSetX, offSetY); + newModel.addAll(...nodes); + for (const node of nodes) { try { + if (node instanceof InputNode && node.hasNoMatchingFields && !node.context) { + // Placeholder node for input search no result found + continue; + } node.setModel(newModel); await node.initPorts(); + if (node instanceof LinkConnectorNode || node instanceof QueryExprConnectorNode) { + continue; + } node.initLinks(); } catch (e) { const errorNodeKind = getErrorKind(node); @@ -125,7 +148,7 @@ export const useDiagramModel = ( isError, refetch, } = useQuery({ - queryKey: ['diagramModel', noOfNodes, zoomLevel, collapsedFields, expandedFields, inputSearch, outputSearch], + queryKey: ['diagramModel', noOfNodes, zoomLevel, collapsedFields, expandedFields, inputSearch, outputSearch, mappings], queryFn: genModel, networkMode: 'always', }); diff --git a/workspaces/ballerina/inline-data-mapper/src/components/styles.ts b/workspaces/ballerina/inline-data-mapper/src/components/styles.ts index ff0c66837ed..fc33b5472c6 100644 --- a/workspaces/ballerina/inline-data-mapper/src/components/styles.ts +++ b/workspaces/ballerina/inline-data-mapper/src/components/styles.ts @@ -21,9 +21,20 @@ import { IO_NODE_DEFAULT_WIDTH, IO_NODE_FIELD_HEIGHT } from "./Diagram/utils/con const typeLabel = { marginLeft: "3px", marginRight: "24px", - padding: "5px", + padding: "5px 8px", minWidth: "100px", - color: "inherit", + color: "var(--vscode-foreground)", + fontFamily: "GilmerRegular", + verticalAlign: "middle", + backgroundColor: "var(--vscode-editor-inactiveSelectionBackground)", + borderRadius: "3px" +}; + +const valueLabel = { + padding: "5px", + fontFamily: "GilmerRegular", + fontSize: "13px", + color: "var(--vscode-foreground)", verticalAlign: "middle", }; @@ -50,20 +61,15 @@ const treeLabel = { }; export const useIONodesStyles = () => ({ - inputTypeLabel: css({ - ...typeLabel - }), - outputTypeLabel: css({ - fontSize: "13px", - fontWeight: 400, + typeLabel: css({ ...typeLabel }), valueLabel: css({ - padding: "5px", - fontWeight: 600, - fontSize: "13px", - color: "inherit", - verticalAlign: "middle", + ...valueLabel + }), + valueLabelHeader: css({ + ...valueLabel, + fontSize: "14px", }), inPort: css({ float: "left", @@ -174,7 +180,7 @@ export const useIONodesStyles = () => ({ requiredMark: css({ color: "var(--vscode-errorForeground)", margin: '0 2px', - fontSize: '13px' + fontSize: '15px' }), treeLabel: css({ ...treeLabel @@ -184,6 +190,10 @@ export const useIONodesStyles = () => ({ height: 'fit-content', flexDirection: "column" }), + subMappingItemLabel: css({ + ...treeLabel, + cursor: "pointer" + }), enumHeaderTreeLabel: css({ verticalAlign: "middle", padding: "5px", @@ -192,12 +202,49 @@ export const useIONodesStyles = () => ({ minHeight: "24px", backgroundColor: "var(--vscode-sideBar-background)" }), + addAnotherSubMappingButton: css({ + width: "auto", + margin: 0, + "& > vscode-button": { + backgroundColor: "var(--vscode-extensionButton-background)", padding: '2px', + "&:hover": { + backgroundColor: "var(--vscode-button-hoverBackground)" + }, + } + }), + subMappingItemSeparator: css({ + height: "2px", + width: "100%", + backgroundColor: "var(--vscode-titleBar-border)", + display: "flex", + alignItems: "center", + justifyContent: "center" + }), objectFieldAdderLabel: css({ display: "flex", justifyContent: "center", color: "var(--button-primary-foreground)", opacity: 0.7 }), + addSubMappingButton: css({ + "& > vscode-button": { + ...addElementButton, + width: `${IO_NODE_DEFAULT_WIDTH}px`, + height: "40px", + border: "1px solid var(--vscode-welcomePage-tileBorder)", + color: "var(--button-primary-foreground)", + opacity: 0.7, + backgroundColor: "var(--vscode-button-secondaryBackground)", + borderRadius: "0px", + textTransform: "none", + "&:hover": { + backgroundColor: "var(--vscode-button-secondaryHoverBackground)" + }, + }, + "& > vscode-button > *": { + margin: "0px 6px" + } + }), addArrayElementButton: css({ "& > vscode-button": { padding: "5px", @@ -228,7 +275,7 @@ export const useIntermediateNodeStyles = () => ({ width: '100%', backgroundColor: "var(--vscode-sideBar-background)", padding: "2px", - borderRadius: "2px", + borderRadius: "3px", display: "flex", flexDirection: "column", gap: "5px", diff --git a/workspaces/ballerina/inline-data-mapper/src/index.tsx b/workspaces/ballerina/inline-data-mapper/src/index.tsx index b9bf32b29c2..09571d93515 100644 --- a/workspaces/ballerina/inline-data-mapper/src/index.tsx +++ b/workspaces/ballerina/inline-data-mapper/src/index.tsx @@ -24,7 +24,7 @@ import type {} from "@projectstorm/react-diagrams-core"; import type {} from "@projectstorm/react-diagrams"; import { css, Global } from '@emotion/react'; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { IDMModel, Mapping } from "@wso2/ballerina-core"; +import { IDMFormProps, IDMViewState, ModelState, IntermediateClause } from "@wso2/ballerina-core"; import { ErrorBoundary } from "@wso2/ui-toolkit"; import { InlineDataMapper } from "./components/DataMapper/DataMapper"; @@ -49,15 +49,20 @@ const globalStyles = css` `; export interface DataMapperViewProps { - model: IDMModel; - applyModifications: (mappings: Mapping[]) => Promise; - addArrayElement: (targetField: string) => Promise; + modelState: ModelState; + name: string; + applyModifications: (outputId: string, expression: string, viewId: string, name: string) => Promise; + addArrayElement: (outputId: string, viewId: string, name: string) => Promise; + generateForm: (formProps: IDMFormProps) => JSX.Element; + convertToQuery: (outputId: string, viewId: string, name: string) => Promise; + addClauses: (clause: IntermediateClause, targetField: string, isNew: boolean, index?:number) => Promise; onClose: () => void; + handleView: (viewId: string, isSubMapping?: boolean) => void; } export function DataMapperView(props: DataMapperViewProps) { return ( - + diff --git a/workspaces/ballerina/inline-data-mapper/src/store/store.ts b/workspaces/ballerina/inline-data-mapper/src/store/store.ts index 516ce0a7870..6ea868ba241 100644 --- a/workspaces/ballerina/inline-data-mapper/src/store/store.ts +++ b/workspaces/ballerina/inline-data-mapper/src/store/store.ts @@ -19,6 +19,18 @@ import { create } from "zustand"; import { InputOutputPortModel } from "../components/Diagram/Port"; +interface SubMappingConfig { + isSMConfigPanelOpen: boolean; + nextSubMappingIndex: number; + suggestedNextSubMappingName: string; +} + +export interface SubMappingConfigFormData { + mappingName: string; + mappingType: string | undefined; + isArray: boolean; +} + export interface DataMapperSearchState { inputSearch: string; setInputSearch: (inputSearch: string) => void; @@ -30,6 +42,8 @@ export interface DataMapperSearchState { export interface DataMapperFieldsState { fields: string[]; setFields: (fields: string[]) => void; + addField: (field: string) => void; + removeField: (field: string) => void; resetFields: () => void; } @@ -42,6 +56,14 @@ export interface DataMapperIOConfigPanelState { setIsSchemaOverridden: (isSchemaOverridden: boolean) => void; } +export interface DataMapperSubMappingConfigPanelState { + subMappingConfig: SubMappingConfig; + setSubMappingConfig: (subMappingConfig: SubMappingConfig) => void; + resetSubMappingConfig: () => void; + subMappingConfigFormData: SubMappingConfigFormData; + setSubMappingConfigFormData: (subMappingConfigFormData: SubMappingConfigFormData) => void +} + export interface DataMapperExpressionBarState { focusedPort: InputOutputPortModel; focusedFilter: Node; @@ -64,12 +86,16 @@ export const useDMSearchStore = create((set) => ({ export const useDMCollapsedFieldsStore = create((set) => ({ fields: [], setFields: (fields: string[]) => set({ fields }), + addField: (field: string) => set((state) => ({ fields: [...state.fields, field] })), + removeField: (field: string) => set((state) => ({ fields: state.fields.filter(f => f !== field) })), resetFields: () => set({ fields: [] }) })); export const useDMExpandedFieldsStore = create((set) => ({ fields: [], setFields: (fields: string[]) => set({ fields }), + addField: (field: string) => set((state) => ({ fields: [...state.fields, field] })), + removeField: (field: string) => set((state) => ({ fields: state.fields.filter(f => f !== field) })), resetFields: () => set({ fields: [] }) })); @@ -82,6 +108,25 @@ export const useDMIOConfigPanelStore = create((set setIsSchemaOverridden: (isSchemaOverridden: boolean) => set({ isSchemaOverridden }), })); +export const useDMSubMappingConfigPanelStore = create((set) => ({ + subMappingConfig: { + isSMConfigPanelOpen: false, + nextSubMappingIndex: -1, + suggestedNextSubMappingName: undefined + }, + setSubMappingConfig: (subMappingConfig: SubMappingConfig) => set({ subMappingConfig }), + resetSubMappingConfig: () => set({ + subMappingConfig: { + isSMConfigPanelOpen: false, + nextSubMappingIndex: -1, + suggestedNextSubMappingName: undefined + }, + subMappingConfigFormData: undefined + }), + subMappingConfigFormData: undefined, + setSubMappingConfigFormData: (subMappingConfigFormData: SubMappingConfigFormData) => set({ subMappingConfigFormData }) +})); + export const useDMExpressionBarStore = create((set) => ({ focusedPort: undefined, focusedFilter: undefined, @@ -92,3 +137,13 @@ export const useDMExpressionBarStore = create((set resetFocus: () => set({ focusedPort: undefined, focusedFilter: undefined }), resetInputPort: () => set({ inputPort: undefined }) })); + +export interface DataMapperQueryClausesPanelState { + isQueryClausesPanelOpen: boolean; + setIsQueryClausesPanelOpen: (isQueryClausesPanelOpen: boolean) => void; +} + +export const useDMQueryClausesPanelStore = create((set) => ({ + isQueryClausesPanelOpen: false, + setIsQueryClausesPanelOpen: (isQueryClausesPanelOpen: boolean) => set({ isQueryClausesPanelOpen }), +})); diff --git a/workspaces/ballerina/inline-data-mapper/src/utils/DataMapperContext/DataMapperContext.ts b/workspaces/ballerina/inline-data-mapper/src/utils/DataMapperContext/DataMapperContext.ts index 86664f25e80..ef5ff694543 100644 --- a/workspaces/ballerina/inline-data-mapper/src/utils/DataMapperContext/DataMapperContext.ts +++ b/workspaces/ballerina/inline-data-mapper/src/utils/DataMapperContext/DataMapperContext.ts @@ -15,24 +15,27 @@ * specific language governing permissions and limitations * under the License. */ -import { IDMModel, Mapping } from "@wso2/ballerina-core"; +import { ExpandedDMModel, Mapping, Query } from "@wso2/ballerina-core"; import { View } from "../../components/DataMapper/Views/DataMapperView"; export interface IDataMapperContext { - model: IDMModel; + model: ExpandedDMModel; views: View[]; addView: (view: View) => void; - applyModifications: (mappings: Mapping[]) => Promise; - addArrayElement: (targetField: string) => Promise; -} + applyModifications: (outputId: string, expression: string, viewId: string, name: string) => Promise; + addArrayElement: (outputId: string, viewId: string, name: string) => Promise; + hasInputsOutputsChanged: boolean; + convertToQuery: (outputId: string, viewId: string, name: string) => Promise;} export class DataMapperContext implements IDataMapperContext { constructor( - public model: IDMModel, + public model: ExpandedDMModel, public views: View[] = [], public addView: (view: View) => void, - public applyModifications: (mappings: Mapping[]) => Promise, - public addArrayElement: (targetField: string) => Promise + 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: (outputId: string, viewId: string, name: string) => Promise ){} } diff --git a/workspaces/ballerina/inline-data-mapper/src/utils/model-utils.ts b/workspaces/ballerina/inline-data-mapper/src/utils/model-utils.ts index e35f1787816..4c0ae1c2e14 100644 --- a/workspaces/ballerina/inline-data-mapper/src/utils/model-utils.ts +++ b/workspaces/ballerina/inline-data-mapper/src/utils/model-utils.ts @@ -16,43 +16,55 @@ * under the License. */ -import { IDMModel, IOType, Mapping } from "@wso2/ballerina-core"; +import { ExpandedDMModel, IOType, Mapping } from "@wso2/ballerina-core"; import { BaseVisitor } from "../visitors/BaseVisitor"; -export function traverseNode(model: IDMModel, visitor: BaseVisitor) { +export function traverseNode(model: ExpandedDMModel, visitor: BaseVisitor) { visitor.beginVisit?.(model); // Visit input types if (model.inputs.length > 0) { for (const inputType of model.inputs) { - traverseIOType(inputType, model, visitor); + traverseInputType(inputType, model, visitor); } } // Visit output type - traverseIOType(model.output, model, visitor); + traverseOutputType(model.output, model, visitor); // Visit mappings traverseMappings(model.mappings, undefined, model, visitor); + // Visit sub mappings + if (model?.subMappings && model.subMappings.length > 0) { + for (const subMapping of model.subMappings) { + traverseSubMappingType(subMapping, model, visitor); + } + } + visitor.endVisit?.(model); } -function traverseIOType(ioType: IOType, parent: IDMModel, visitor: BaseVisitor) { - if (!!ioType.category) { - visitor.beginVisitInputType?.(ioType, parent); - visitor.endVisitInputType?.(ioType, parent); - } else { - visitor.beginVisitOutputType?.(ioType, parent); - visitor.endVisitOutputType?.(ioType, parent); - } +function traverseInputType(ioType: IOType, parent: ExpandedDMModel, visitor: BaseVisitor) { + visitor.beginVisitInputType?.(ioType, parent); + visitor.endVisitInputType?.(ioType, parent); +} + +function traverseOutputType(ioType: IOType, parent: ExpandedDMModel, visitor: BaseVisitor) { + visitor.beginVisitOutputType?.(ioType, parent); + visitor.endVisitOutputType?.(ioType, parent); +} + +function traverseSubMappingType(ioType: IOType, parent: ExpandedDMModel, visitor: BaseVisitor) { + visitor.beginVisitSubMappingType?.(ioType, parent); + visitor.endVisitSubMappingType?.(ioType, parent); } -function traverseMappings(mappings: Mapping[], parentMapping: Mapping, parentModel: IDMModel, visitor: BaseVisitor) { +function traverseMappings(mappings: Mapping[], parentMapping: Mapping, parentModel: ExpandedDMModel, visitor: BaseVisitor) { for (const mapping of mappings) { visitor.beginVisitMapping?.(mapping, parentMapping, parentModel); - if (mapping?.elements.length > 0) { + if (mapping?.elements && mapping.elements.length > 0) { const mappingElelements = mapping.elements; for (const element of mappingElelements) { diff --git a/workspaces/ballerina/inline-data-mapper/src/visitors/BaseVisitor.ts b/workspaces/ballerina/inline-data-mapper/src/visitors/BaseVisitor.ts index f85ed422fd8..d0f5d48b952 100644 --- a/workspaces/ballerina/inline-data-mapper/src/visitors/BaseVisitor.ts +++ b/workspaces/ballerina/inline-data-mapper/src/visitors/BaseVisitor.ts @@ -16,18 +16,21 @@ * under the License. */ -import { IDMModel, IOType, Mapping } from "@wso2/ballerina-core"; +import { ExpandedDMModel, IOType, Mapping } from "@wso2/ballerina-core"; export interface BaseVisitor { - beginVisit?(node: IDMModel, parent?: IDMModel): void; - endVisit?(node: IDMModel, parent?: IDMModel): void; + beginVisit?(node: ExpandedDMModel, parent?: ExpandedDMModel): void; + endVisit?(node: ExpandedDMModel, parent?: ExpandedDMModel): void; - beginVisitInputType?(node: IOType, parent?: IDMModel): void; - endVisitInputType?(node: IOType, parent?: IDMModel): void; + beginVisitInputType?(node: IOType, parent?: ExpandedDMModel): void; + endVisitInputType?(node: IOType, parent?: ExpandedDMModel): void; - beginVisitOutputType?(node: IOType, parent?: IDMModel): void; - endVisitOutputType?(node: IOType, parent?: IDMModel): void; + beginVisitOutputType?(node: IOType, parent?: ExpandedDMModel): void; + endVisitOutputType?(node: IOType, parent?: ExpandedDMModel): void; - beginVisitMapping?(node: Mapping, parentMapping: Mapping, parentModel?: IDMModel): void; - endVisitMapping?(node: Mapping, parentMapping: Mapping, parentModel?: IDMModel): void; + beginVisitSubMappingType?(node: IOType, parent?: ExpandedDMModel): void; + endVisitSubMappingType?(node: IOType, parent?: ExpandedDMModel): void; + + beginVisitMapping?(node: Mapping, parentMapping: Mapping, parentModel?: ExpandedDMModel): void; + endVisitMapping?(node: Mapping, parentMapping: Mapping, parentModel?: ExpandedDMModel): void; } diff --git a/workspaces/ballerina/inline-data-mapper/src/visitors/IONodeInitVisitor.ts b/workspaces/ballerina/inline-data-mapper/src/visitors/IONodeInitVisitor.ts new file mode 100644 index 00000000000..d2d37003797 --- /dev/null +++ b/workspaces/ballerina/inline-data-mapper/src/visitors/IONodeInitVisitor.ts @@ -0,0 +1,63 @@ +/** + * 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 { ArrayOutputNode, EmptyInputsNode, InputNode, ObjectOutputNode, PrimitiveOutputNode, QueryOutputNode } from "../components/Diagram/Node"; +import { DataMapperNodeModel } from "../components/Diagram/Node/commons/DataMapperNode"; +import { DataMapperContext } from "../utils/DataMapperContext/DataMapperContext"; +import { ExpandedDMModel, IOType, TypeKind } from "@wso2/ballerina-core"; +import { OFFSETS } from "../components/Diagram/utils/constants"; +import { BaseVisitor } from "./BaseVisitor"; + +export class IONodeInitVisitor implements BaseVisitor { + private inputNodes: DataMapperNodeModel[] = []; + private outputNode: DataMapperNodeModel; + + constructor( + private context: DataMapperContext, + ){} + + beginVisitInputType(node: IOType, parent?: ExpandedDMModel): void { + // Create input node + const inputNode = new InputNode(this.context, node); + inputNode.setPosition(0, 0); + this.inputNodes.push(inputNode); + } + + beginVisitOutputType(node: IOType, parent?: ExpandedDMModel): void { + // Create output node + if (node.kind === TypeKind.Record) { + this.outputNode = new ObjectOutputNode(this.context, node); + } else if (node.kind === TypeKind.Array) { + if (parent?.query) { + this.outputNode = new QueryOutputNode(this.context, node); + } else { + this.outputNode = new ArrayOutputNode(this.context, node); + } + } else { + this.outputNode = new PrimitiveOutputNode(this.context, node); + } + // TODO: Handle other types + this.outputNode.setPosition(OFFSETS.TARGET_NODE.X, OFFSETS.TARGET_NODE.Y); + } + + getNodes() { + if (this.inputNodes.length === 0) { + this.inputNodes.push(new EmptyInputsNode()); + } + return [...this.inputNodes, this.outputNode]; + } +} diff --git a/workspaces/ballerina/inline-data-mapper/src/visitors/IntermediateNodeInitVisitor.ts b/workspaces/ballerina/inline-data-mapper/src/visitors/IntermediateNodeInitVisitor.ts new file mode 100644 index 00000000000..3691af65e7b --- /dev/null +++ b/workspaces/ballerina/inline-data-mapper/src/visitors/IntermediateNodeInitVisitor.ts @@ -0,0 +1,75 @@ +/** + * 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 { LinkConnectorNode } 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 { BaseVisitor } from "./BaseVisitor"; +import { QueryExprConnectorNode } from "../components/Diagram/Node/QueryExprConnector"; + +export class IntermediateNodeInitVisitor implements BaseVisitor { + private intermediateNodes: DataMapperNodeModel[] = []; + private existingNodes: DataMapperNodeModel[]; + + constructor( + private context: DataMapperContext, + existingNodes: DataMapperNodeModel[] = [] + ){ + this.existingNodes = existingNodes; + } + + beginVisitMapping(node: Mapping): void { + // If inputs/outputs haven't changed, try to find existing node + if (!this.context.hasInputsOutputsChanged) { + const existingNode = this.findExistingNode(node.output); + if (existingNode) { + console.log(">>> [IntermediateNodeInitVisitor] found existing intermediatenode:", existingNode); + existingNode.context = this.context; + this.intermediateNodes.push(existingNode); + return; + } + } + + // Create new node if no existing node found or inputs/outputs changed + if (node.isQueryExpression) { + // Create query expression connector node + const queryExprNode = new QueryExprConnectorNode(this.context, node); + this.intermediateNodes.push(queryExprNode); + } else if (node.inputs.length > 1 || node.isComplex || node.isFunctionCall) { + // Create link connector node + const linkConnectorNode = new LinkConnectorNode(this.context, node); + this.intermediateNodes.push(linkConnectorNode); + } + } + + getNodes() { + return this.intermediateNodes; + } + + private findExistingNode(targetField: string): DataMapperNodeModel | undefined { + return this.existingNodes.find(node => { + if (node instanceof LinkConnectorNode) { + return node.mapping.output === targetField; + } + if (node instanceof QueryExprConnectorNode) { + return node.mapping.output === targetField; + } + return false; + }); + } +} diff --git a/workspaces/ballerina/inline-data-mapper/src/visitors/SubMappingNodeInitVisitor.ts b/workspaces/ballerina/inline-data-mapper/src/visitors/SubMappingNodeInitVisitor.ts new file mode 100644 index 00000000000..cccd30605d7 --- /dev/null +++ b/workspaces/ballerina/inline-data-mapper/src/visitors/SubMappingNodeInitVisitor.ts @@ -0,0 +1,41 @@ +/** + * 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 { DataMapperNodeModel } from "../components/Diagram/Node/commons/DataMapperNode"; +import { DataMapperContext } from "../utils/DataMapperContext/DataMapperContext"; +import { IOType } from "@wso2/ballerina-core"; +import { BaseVisitor } from "./BaseVisitor"; +import { DMSubMapping, SubMappingNode } from "../components/Diagram/Node/SubMapping/SubMappingNode"; + +export class SubMappingNodeInitVisitor implements BaseVisitor { + private subMappingNode: DataMapperNodeModel; + private subMappings: DMSubMapping[] = []; + + constructor(private context: DataMapperContext){} + + beginVisitSubMappingType(node: IOType): void { + this.subMappings.push({ + name: node.id, + type: node + }); + } + + getNode() { + this.subMappingNode = new SubMappingNode(this.context, this.subMappings); + return this.subMappingNode; + } +} diff --git a/workspaces/common-libs/font-wso2-vscode/src/icons/map-array.svg b/workspaces/common-libs/font-wso2-vscode/src/icons/map-array.svg new file mode 100644 index 00000000000..2685fe07e1b --- /dev/null +++ b/workspaces/common-libs/font-wso2-vscode/src/icons/map-array.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/workspaces/common-libs/ui-toolkit/src/components/SidePanel/SidePanel.stories.tsx b/workspaces/common-libs/ui-toolkit/src/components/SidePanel/SidePanel.stories.tsx index c0a37da7087..1c5107f7f63 100644 --- a/workspaces/common-libs/ui-toolkit/src/components/SidePanel/SidePanel.stories.tsx +++ b/workspaces/common-libs/ui-toolkit/src/components/SidePanel/SidePanel.stories.tsx @@ -122,3 +122,58 @@ export const WithContent: Story = { ); }, }; + +export const SidePanelWithSubPanel: Story = { + render: () => { + const [isOpen, setIsOpen] = React.useState(false); + const [isSubPanelOpen, setIsSubPanelOpen] = React.useState(false); + + const openPanel = () => { + setIsOpen(!isOpen); + }; + const openSubPanel = () => { + setIsSubPanelOpen(!isSubPanelOpen); + }; + const closePanel = () => { + setIsOpen(false); + }; + const closeSubPanel = () => { + setIsSubPanelOpen(false); + }; + + const subPanel = isSubPanelOpen && ( +
+ +
Sub Panel Title
+ +
+
+ ); + + + return ( + <> +
+ Click to Open Side Panel +
+ + +
Side Panel Title
+ +
+ +
+ Click to Open Sub Panel +
+
+
+ + ); + } +}; diff --git a/workspaces/common-libs/ui-toolkit/src/components/SidePanel/SidePanel.tsx b/workspaces/common-libs/ui-toolkit/src/components/SidePanel/SidePanel.tsx index 2aa07673d01..43cbe70bf36 100644 --- a/workspaces/common-libs/ui-toolkit/src/components/SidePanel/SidePanel.tsx +++ b/workspaces/common-libs/ui-toolkit/src/components/SidePanel/SidePanel.tsx @@ -64,7 +64,7 @@ const SubPanelContainer = styled.div` position: fixed; top: 0; ${(props: SidePanelProps) => props.alignment === "left" ? "left" : "right"}: ${(props: SidePanelProps) => `${props.width}px`}; - width: ${(props: SidePanelProps) => `${props.subPanelWidth}px`}; + width: ${(props: SidePanelProps) => props?.subPanelWidth ? `${props?.subPanelWidth}px` : `calc(100vw - ${props.width}px)`}; height: 100%; box-shadow: 0 5px 10px 0 var(--vscode-badge-background); background-color: var(--vscode-editor-background); From b2f59280969d09551b6f1547e10dc5680180f83e Mon Sep 17 00:00:00 2001 From: madushajg Date: Mon, 7 Jul 2025 13:36:55 +0530 Subject: [PATCH 5/6] Resolve issues caused by branch merge --- common/config/rush/pnpm-lock.yaml | 179 ++++++++---------- .../ballerina-extension/package.json | 4 +- .../src/components/Form/index.tsx | 1 - .../src/visitors/NodeInitVisitor.ts | 69 ------- .../choreo/choreo-extension/package.json | 2 +- workspaces/mi/mi-extension/package.json | 4 +- 6 files changed, 85 insertions(+), 174 deletions(-) delete mode 100644 workspaces/ballerina/inline-data-mapper/src/visitors/NodeInitVisitor.ts diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 2f890744c80..181e76c07cb 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -186,7 +186,7 @@ importers: version: 2.1.1 portfinder: specifier: ^1.0.32 - version: 1.0.37 + version: 1.0.37(supports-color@5.5.0) source-map-support: specifier: ^0.5.21 version: 0.5.21 @@ -1514,9 +1514,6 @@ importers: '@wso2/ballerina-rpc-client': specifier: workspace:* version: link:../ballerina-rpc-client - '@wso2/syntax-tree': - specifier: workspace:* - version: link:../syntax-tree '@wso2/ui-toolkit': specifier: workspace:* version: link:../../common-libs/ui-toolkit @@ -3487,7 +3484,7 @@ importers: version: 2.1.0(webpack@5.99.9) portfinder: specifier: ^1.0.37 - version: 1.0.37 + version: 1.0.37(supports-color@5.5.0) recast: specifier: ^0.23.11 version: 0.23.11 @@ -23508,7 +23505,7 @@ snapshots: '@babel/traverse': 7.27.4 '@babel/types': 7.27.6 convert-source-map: 1.9.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) gensync: 1.0.0-beta.2 json5: 2.2.3 lodash: 4.17.21 @@ -23531,7 +23528,7 @@ snapshots: '@babel/traverse': 7.27.4 '@babel/types': 7.27.6 convert-source-map: 2.0.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -23585,7 +23582,7 @@ snapshots: '@babel/helper-module-imports': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 '@babel/traverse': 7.27.4 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) lodash.debounce: 4.0.8 resolve: 1.22.10 semver: 6.3.1 @@ -23599,7 +23596,7 @@ snapshots: '@babel/helper-module-imports': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 '@babel/traverse': 7.27.4 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) lodash.debounce: 4.0.8 resolve: 1.22.10 semver: 6.3.1 @@ -23611,7 +23608,7 @@ snapshots: '@babel/core': 7.27.4 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-plugin-utils': 7.27.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) lodash.debounce: 4.0.8 resolve: 1.22.10 transitivePeerDependencies: @@ -24456,7 +24453,7 @@ snapshots: '@babel/parser': 7.27.5 '@babel/template': 7.27.2 '@babel/types': 7.27.6 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -24959,7 +24956,7 @@ snapshots: '@eslint/config-array@0.20.1': dependencies: '@eslint/object-schema': 2.1.6 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -24981,7 +24978,7 @@ snapshots: '@eslint/eslintrc@2.1.4': dependencies: ajv: 6.12.6 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) espree: 9.6.1 globals: 13.24.0 ignore: 5.3.2 @@ -24995,7 +24992,7 @@ snapshots: '@eslint/eslintrc@3.3.1': dependencies: ajv: 6.12.6 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 @@ -25192,7 +25189,7 @@ snapshots: '@humanwhocodes/config-array@0.13.0': dependencies: '@humanwhocodes/object-schema': 2.0.3 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -27150,7 +27147,7 @@ snapshots: '@secretlint/resolver': 9.3.4 '@secretlint/types': 9.3.4 ajv: 8.17.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) rc-config-loader: 4.1.3 transitivePeerDependencies: - supports-color @@ -27159,7 +27156,7 @@ snapshots: dependencies: '@secretlint/profiler': 9.3.4 '@secretlint/types': 9.3.4 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) structured-source: 4.0.0 transitivePeerDependencies: - supports-color @@ -27172,7 +27169,7 @@ snapshots: '@textlint/module-interop': 14.8.4 '@textlint/types': 14.8.4 chalk: 4.1.2 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) pluralize: 8.0.0 strip-ansi: 6.0.1 table: 6.9.0 @@ -27188,7 +27185,7 @@ snapshots: '@secretlint/profiler': 9.3.4 '@secretlint/source-creator': 9.3.4 '@secretlint/types': 9.3.4 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) p-map: 4.0.0 transitivePeerDependencies: - supports-color @@ -29077,7 +29074,7 @@ snapshots: semver: 7.7.2 storybook: 9.0.12(@testing-library/dom@10.4.0)(prettier@3.5.3) style-loader: 3.3.4(webpack@5.99.9(webpack-cli@5.1.4)) - terser-webpack-plugin: 5.3.14(webpack@5.99.9(webpack-cli@5.1.4)) + terser-webpack-plugin: 5.3.14(webpack@5.99.9) ts-dedent: 2.2.0 url: 0.11.4 util: 0.12.5 @@ -31170,7 +31167,7 @@ snapshots: '@storybook/react-docgen-typescript-plugin@1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0(typescript@4.9.5)(webpack@5.99.9)': dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) endent: 2.1.0 find-cache-dir: 3.3.2 flat-cache: 3.2.0 @@ -31184,7 +31181,7 @@ snapshots: '@storybook/react-docgen-typescript-plugin@1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0(typescript@5.8.3)(webpack@5.99.9)': dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) endent: 2.1.0 find-cache-dir: 3.3.2 flat-cache: 3.2.0 @@ -31198,7 +31195,7 @@ snapshots: '@storybook/react-docgen-typescript-plugin@1.0.6--canary.9.0c3f3b7.0(typescript@5.8.3)(webpack@5.99.9(webpack-cli@5.1.4))': dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) endent: 2.1.0 find-cache-dir: 3.3.2 flat-cache: 3.2.0 @@ -31212,7 +31209,7 @@ snapshots: '@storybook/react-docgen-typescript-plugin@1.0.6--canary.9.0c3f3b7.0(typescript@5.8.3)(webpack@5.99.9)': dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) endent: 2.1.0 find-cache-dir: 3.3.2 flat-cache: 3.2.0 @@ -32640,7 +32637,7 @@ snapshots: '@textlint/resolver': 14.8.4 '@textlint/types': 14.8.4 chalk: 4.1.2 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) js-yaml: 3.14.1 lodash: 4.17.21 pluralize: 2.0.0 @@ -33246,7 +33243,7 @@ snapshots: '@typescript-eslint/scope-manager': 5.48.2 '@typescript-eslint/type-utils': 5.48.2(eslint@8.57.1)(typescript@5.8.3) '@typescript-eslint/utils': 5.48.2(eslint@8.57.1)(typescript@5.8.3) - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.1 ignore: 5.3.2 natural-compare-lite: 1.4.0 @@ -33266,7 +33263,7 @@ snapshots: '@typescript-eslint/type-utils': 6.21.0(eslint@8.57.1)(typescript@5.8.3) '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@5.8.3) '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.1 graphemer: 1.4.0 ignore: 5.3.2 @@ -33286,7 +33283,7 @@ snapshots: '@typescript-eslint/type-utils': 6.21.0(eslint@8.57.1)(typescript@5.8.3) '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@5.8.3) '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.1 graphemer: 1.4.0 ignore: 5.3.2 @@ -33412,7 +33409,7 @@ snapshots: '@typescript-eslint/scope-manager': 5.48.2 '@typescript-eslint/types': 5.48.2 '@typescript-eslint/typescript-estree': 5.48.2(typescript@5.8.3) - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.1 optionalDependencies: typescript: 5.8.3 @@ -33425,7 +33422,7 @@ snapshots: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.8.3) '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.1 optionalDependencies: typescript: 5.8.3 @@ -33438,7 +33435,7 @@ snapshots: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.8.3) '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.1 optionalDependencies: typescript: 5.8.3 @@ -33451,7 +33448,7 @@ snapshots: '@typescript-eslint/types': 8.32.1 '@typescript-eslint/typescript-estree': 8.32.1(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.32.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 9.27.0(jiti@2.4.2) typescript: 5.8.3 transitivePeerDependencies: @@ -33463,7 +33460,7 @@ snapshots: '@typescript-eslint/types': 8.33.1 '@typescript-eslint/typescript-estree': 8.33.1(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.33.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.1 typescript: 5.8.3 transitivePeerDependencies: @@ -33475,7 +33472,7 @@ snapshots: '@typescript-eslint/types': 8.33.1 '@typescript-eslint/typescript-estree': 8.33.1(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.33.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 9.26.0(jiti@2.4.2) typescript: 5.8.3 transitivePeerDependencies: @@ -33487,7 +33484,7 @@ snapshots: '@typescript-eslint/types': 8.33.1 '@typescript-eslint/typescript-estree': 8.33.1(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.33.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 9.27.0(jiti@2.4.2) typescript: 5.8.3 transitivePeerDependencies: @@ -33497,7 +33494,7 @@ snapshots: dependencies: '@typescript-eslint/tsconfig-utils': 8.33.1(typescript@5.8.3) '@typescript-eslint/types': 8.33.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -33535,7 +33532,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 5.48.2(typescript@5.8.3) '@typescript-eslint/utils': 5.48.2(eslint@8.57.1)(typescript@5.8.3) - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.1 tsutils: 3.21.0(typescript@5.8.3) optionalDependencies: @@ -33547,7 +33544,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.8.3) '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@5.8.3) - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.1 ts-api-utils: 1.4.3(typescript@5.8.3) optionalDependencies: @@ -33559,7 +33556,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.8.3) '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.8.3) - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.1 ts-api-utils: 1.4.3(typescript@5.8.3) optionalDependencies: @@ -33571,7 +33568,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 8.32.1(typescript@5.8.3) '@typescript-eslint/utils': 8.32.1(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3) - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 9.26.0(jiti@2.4.2) ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 @@ -33582,7 +33579,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 8.32.1(typescript@5.8.3) '@typescript-eslint/utils': 8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 9.27.0(jiti@2.4.2) ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 @@ -33593,7 +33590,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 8.33.1(typescript@5.8.3) '@typescript-eslint/utils': 8.33.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 9.27.0(jiti@2.4.2) ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 @@ -33612,7 +33609,7 @@ snapshots: '@typescript-eslint/typescript-estree@2.34.0(typescript@3.9.10)': dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint-visitor-keys: 1.3.0 glob: 7.2.3 is-glob: 4.0.3 @@ -33628,7 +33625,7 @@ snapshots: dependencies: '@typescript-eslint/types': 5.48.2 '@typescript-eslint/visitor-keys': 5.48.2 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 semver: 7.7.2 @@ -33642,7 +33639,7 @@ snapshots: dependencies: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 @@ -33657,7 +33654,7 @@ snapshots: dependencies: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.5 @@ -33672,7 +33669,7 @@ snapshots: dependencies: '@typescript-eslint/types': 8.32.1 '@typescript-eslint/visitor-keys': 8.32.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 @@ -33688,7 +33685,7 @@ snapshots: '@typescript-eslint/tsconfig-utils': 8.33.1(typescript@5.8.3) '@typescript-eslint/types': 8.33.1 '@typescript-eslint/visitor-keys': 8.33.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 @@ -34417,7 +34414,7 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -35765,7 +35762,7 @@ snapshots: dependencies: bytes: 3.1.2 content-type: 1.0.5 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) http-errors: 2.0.0 iconv-lite: 0.6.3 on-finished: 2.4.1 @@ -37657,7 +37654,7 @@ snapshots: detect-port@1.6.1: dependencies: address: 1.2.2 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -38146,14 +38143,14 @@ snapshots: esbuild-register@3.6.0(esbuild@0.18.20): dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) esbuild: 0.18.20 transitivePeerDependencies: - supports-color esbuild-register@3.6.0(esbuild@0.25.5): dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) esbuild: 0.25.5 transitivePeerDependencies: - supports-color @@ -38481,7 +38478,7 @@ snapshots: ajv: 6.12.6 chalk: 2.4.2 cross-spawn: 6.0.6 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) doctrine: 3.0.0 eslint-scope: 5.1.1 eslint-utils: 1.4.3 @@ -38530,7 +38527,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -38579,7 +38576,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) escape-string-regexp: 4.0.0 eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 @@ -38622,7 +38619,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) escape-string-regexp: 4.0.0 eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 @@ -38937,7 +38934,7 @@ snapshots: content-type: 1.0.5 cookie: 0.7.2 cookie-signature: 1.2.2 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -39291,7 +39288,7 @@ snapshots: finalhandler@2.1.0: dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 @@ -40725,7 +40722,7 @@ snapshots: dependencies: '@tootallnate/once': 1.1.2 agent-base: 6.0.2 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -40733,14 +40730,14 @@ snapshots: dependencies: '@tootallnate/once': 2.0.0 agent-base: 6.0.2 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.3 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -40789,7 +40786,7 @@ snapshots: mime: 1.6.0 minimist: 1.2.8 opener: 1.5.2 - portfinder: 1.0.37 + portfinder: 1.0.37(supports-color@5.5.0) secure-compare: 3.0.1 union: 0.5.0 url-join: 4.0.1 @@ -40813,21 +40810,21 @@ snapshots: https-proxy-agent@4.0.0: dependencies: agent-base: 5.1.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.3 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -41530,7 +41527,7 @@ snapshots: istanbul-lib-source-maps@4.0.1: dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: @@ -43006,7 +43003,7 @@ snapshots: dependencies: chalk: 5.4.1 commander: 14.0.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) lilconfig: 3.1.3 listr2: 8.3.3 micromatch: 4.0.8 @@ -44016,7 +44013,7 @@ snapshots: micromark@3.2.0: dependencies: '@types/debug': 4.1.12 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) decode-named-character-reference: 1.2.0 micromark-core-commonmark: 1.1.0 micromark-factory-space: 1.1.0 @@ -44038,7 +44035,7 @@ snapshots: micromark@4.0.2: dependencies: '@types/debug': 4.1.12 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) decode-named-character-reference: 1.2.0 devlop: 1.1.0 micromark-core-commonmark: 2.0.3 @@ -45428,13 +45425,6 @@ snapshots: style-value-types: 5.0.0 tslib: 2.8.1 - portfinder@1.0.37: - dependencies: - async: 3.2.6 - debug: 4.4.1(supports-color@8.1.1) - transitivePeerDependencies: - - supports-color - portfinder@1.0.37(supports-color@5.5.0): dependencies: async: 3.2.6 @@ -46185,7 +46175,7 @@ snapshots: puppeteer-core@2.1.1: dependencies: '@types/mime-types': 2.1.4 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) extract-zip: 1.7.0 https-proxy-agent: 4.0.0 mime: 2.6.0 @@ -46298,7 +46288,7 @@ snapshots: rc-config-loader@4.1.3: dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) js-yaml: 4.1.0 json5: 2.2.3 require-from-string: 2.0.2 @@ -47519,7 +47509,7 @@ snapshots: router@2.2.0: dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) depd: 2.0.0 is-promise: 4.0.0 parseurl: 1.3.3 @@ -47714,7 +47704,7 @@ snapshots: '@secretlint/formatter': 9.3.4 '@secretlint/node': 9.3.4 '@secretlint/profiler': 9.3.4 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) globby: 14.1.0 read-pkg: 8.1.0 transitivePeerDependencies: @@ -47773,7 +47763,7 @@ snapshots: send@1.2.0: dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -48065,7 +48055,7 @@ snapshots: socks-proxy-agent@6.2.1: dependencies: agent-base: 6.0.2 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) socks: 2.8.5 transitivePeerDependencies: - supports-color @@ -48073,7 +48063,7 @@ snapshots: socks-proxy-agent@7.0.0: dependencies: agent-base: 6.0.2 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) socks: 2.8.5 transitivePeerDependencies: - supports-color @@ -48184,7 +48174,7 @@ snapshots: spdy-transport@3.0.0: dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) detect-node: 2.1.0 hpack.js: 2.1.6 obuf: 1.1.2 @@ -48204,7 +48194,7 @@ snapshots: spdy@4.0.2: dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) handle-thing: 2.0.1 http-deceiver: 1.2.7 select-hose: 2.0.0 @@ -48363,7 +48353,7 @@ snapshots: streamroller@3.1.5: dependencies: date-format: 4.0.14 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) fs-extra: 8.1.0 transitivePeerDependencies: - supports-color @@ -48669,7 +48659,7 @@ snapshots: cosmiconfig: 9.0.0(typescript@5.8.3) css-functions-list: 3.2.3 css-tree: 3.1.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) fast-glob: 3.3.3 fastest-levenshtein: 1.0.16 file-entry-cache: 10.1.1 @@ -49225,15 +49215,6 @@ snapshots: terser: 5.43.0 webpack: 5.88.2(webpack-cli@5.1.4) - terser-webpack-plugin@5.3.14(webpack@5.99.9(webpack-cli@5.1.4)): - dependencies: - '@jridgewell/trace-mapping': 0.3.25 - jest-worker: 27.5.1 - schema-utils: 4.3.2 - serialize-javascript: 6.0.2 - terser: 5.43.0 - webpack: 5.99.9(webpack-cli@5.1.4) - terser-webpack-plugin@5.3.14(webpack@5.99.9): dependencies: '@jridgewell/trace-mapping': 0.3.25 @@ -49241,7 +49222,7 @@ snapshots: schema-utils: 4.3.2 serialize-javascript: 6.0.2 terser: 5.43.0 - webpack: 5.99.9(webpack-cli@6.0.1) + webpack: 5.99.9(webpack-cli@5.1.4) terser@4.8.1: dependencies: @@ -49591,7 +49572,7 @@ snapshots: semver: 7.7.2 source-map: 0.7.4 typescript: 5.8.3 - webpack: 5.99.9(webpack-cli@6.0.1) + webpack: 5.99.9(webpack-cli@5.1.4) ts-mixer@6.0.4: {} diff --git a/workspaces/ballerina/ballerina-extension/package.json b/workspaces/ballerina/ballerina-extension/package.json index 76bb3fdd315..03bc3aa14d9 100644 --- a/workspaces/ballerina/ballerina-extension/package.json +++ b/workspaces/ballerina/ballerina-extension/package.json @@ -913,7 +913,7 @@ "description": "design-view", "default": { "fontPath": "./resources/font-wso2-vscode/dist/wso2-vscode.woff", - "fontCharacter": "\\f16b" + "fontCharacter": "\\f16c" } }, "distro-start": { @@ -927,7 +927,7 @@ "description": "debug", "default": { "fontPath": "./resources/font-wso2-vscode/dist/wso2-vscode.woff", - "fontCharacter": "\\f16e" + "fontCharacter": "\\f16f" } }, "distro-source-view": { diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/Form/index.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/Form/index.tsx index 36505c12ea3..a3b593a6315 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/Form/index.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/Form/index.tsx @@ -743,7 +743,6 @@ export const Form = forwardRef((props: FormProps, ref) => { subPanelView={subPanelView} handleOnFieldFocus={handleOnFieldFocus} autoFocus={firstEditableFieldIndex === formFields.indexOf(updatedField)} - visualizableFields={visualizableFields} recordTypeFields={recordTypeFields} onIdentifierEditingStateChange={handleIdentifierEditingStateChange} /> diff --git a/workspaces/ballerina/inline-data-mapper/src/visitors/NodeInitVisitor.ts b/workspaces/ballerina/inline-data-mapper/src/visitors/NodeInitVisitor.ts deleted file mode 100644 index d8120b09d11..00000000000 --- a/workspaces/ballerina/inline-data-mapper/src/visitors/NodeInitVisitor.ts +++ /dev/null @@ -1,69 +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. - */ -import { ArrayOutputNode, EmptyInputsNode, InputNode, LinkConnectorNode, ObjectOutputNode } from "../components/Diagram/Node"; -import { DataMapperNodeModel } from "../components/Diagram/Node/commons/DataMapperNode"; -import { DataMapperContext } from "../utils/DataMapperContext/DataMapperContext"; -import { IDMModel, IOType, Mapping, TypeKind } from "@wso2/ballerina-core"; -import { OFFSETS } from "../components/Diagram/utils/constants"; -import { BaseVisitor } from "./BaseVisitor"; - -export class NodeInitVisitor implements BaseVisitor { - private inputNodes: DataMapperNodeModel[] = []; - private outputNode: DataMapperNodeModel; - private intermediateNodes: DataMapperNodeModel[] = []; - - constructor( - private context: DataMapperContext, - ){} - - beginVisitInputType(node: IOType, parent?: IDMModel): void { - // Create input node - const inputNode = new InputNode(this.context, node); - inputNode.setPosition(0, 0); - this.inputNodes.push(inputNode); - } - - beginVisitOutputType(node: IOType, parent?: IDMModel): void { - // Create output node - if (node.kind === TypeKind.Record) { - this.outputNode = new ObjectOutputNode(this.context, node); - } else if (node.kind === TypeKind.Array) { - this.outputNode = new ArrayOutputNode(this.context, node); - } - // TODO: Handle other types - this.outputNode.setPosition(OFFSETS.TARGET_NODE.X, OFFSETS.TARGET_NODE.Y); - } - - beginVisitMapping(node: Mapping, parentMapping: Mapping, parentModel?: IDMModel): void { - // Create link connector node - if (node.inputs.length > 1 || node.isComplex || node.isFunctionCall) { - // Create intermediate node - const intermediateNode = new LinkConnectorNode(this.context, node); - this.intermediateNodes.push(intermediateNode); - } - } - - getNodes() { - if (this.inputNodes.length === 0) { - this.inputNodes.push(new EmptyInputsNode()); - } - const nodes = [...this.inputNodes, this.outputNode]; - nodes.push(...this.intermediateNodes); - return nodes; - } -} diff --git a/workspaces/choreo/choreo-extension/package.json b/workspaces/choreo/choreo-extension/package.json index 3950edbaf6c..22b28a1ea80 100644 --- a/workspaces/choreo/choreo-extension/package.json +++ b/workspaces/choreo/choreo-extension/package.json @@ -158,7 +158,7 @@ "description": "choreo-2", "default": { "fontPath": "./resources/font-wso2-vscode/dist/wso2-vscode.woff", - "fontCharacter": "\\f17d" + "fontCharacter": "\\f17e" } } } diff --git a/workspaces/mi/mi-extension/package.json b/workspaces/mi/mi-extension/package.json index 37d27ab5717..d7195bc3aec 100644 --- a/workspaces/mi/mi-extension/package.json +++ b/workspaces/mi/mi-extension/package.json @@ -877,14 +877,14 @@ "description": "design-view", "default": { "fontPath": "./resources/font-wso2-vscode/dist/wso2-vscode.woff", - "fontCharacter": "\\f16b" + "fontCharacter": "\\f16c" } }, "distro-build-package": { "description": "build-package", "default": { "fontPath": "./resources/font-wso2-vscode/dist/wso2-vscode.woff", - "fontCharacter": "\\f182" + "fontCharacter": "\\f183" } } } From 6a1e1aaeeef402180b08c800e4e355e6db90a30d Mon Sep 17 00:00:00 2001 From: madushajg Date: Mon, 7 Jul 2025 14:50:49 +0530 Subject: [PATCH 6/6] Fix indentation --- common/scripts/env-webpack-helper.js | 61 ++++++++++++++-------------- 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/common/scripts/env-webpack-helper.js b/common/scripts/env-webpack-helper.js index 7e9398e0123..32afa44f283 100644 --- a/common/scripts/env-webpack-helper.js +++ b/common/scripts/env-webpack-helper.js @@ -26,35 +26,34 @@ * @returns {Object} Environment variables object ready for webpack.DefinePlugin */ function createEnvDefinePlugin(env) { - - const envKeys = Object.create(null); - const missingVars = []; - - if (env) { - Object.entries(env).forEach(([key, value]) => { - if (value !== undefined && value !== '') { - envKeys[`process.env.${key}`] = JSON.stringify(value); - } - else if (process.env[key] !== undefined && process.env[key] !== '') { - envKeys[`process.env.${key}`] = JSON.stringify(process.env[key]); - } - else { - missingVars.push(key); - } - }); - } - - if (missingVars.length > 0) { - throw new Error( - `Missing required environment variables: ${missingVars.join(', ')}\n` + - `Please provide values in either .env file or runtime environment.\n` - ); - } - - return envKeys; + + const envKeys = Object.create(null); + const missingVars = []; + + if (env) { + Object.entries(env).forEach(([key, value]) => { + if (value !== undefined && value !== '') { + envKeys[`process.env.${key}`] = JSON.stringify(value); + } + else if (process.env[key] !== undefined && process.env[key] !== '') { + envKeys[`process.env.${key}`] = JSON.stringify(process.env[key]); + } + else { + missingVars.push(key); + } + }); + } + + if (missingVars.length > 0) { + throw new Error( + `Missing required environment variables: ${missingVars.join(', ')}\n` + + `Please provide values in either .env file or runtime environment.\n` + ); } - - module.exports = { - createEnvDefinePlugin - }; - \ No newline at end of file + + return envKeys; +} + +module.exports = { + createEnvDefinePlugin +};