diff --git a/workspaces/ballerina/ballerina-core/src/interfaces/data-mapper.ts b/workspaces/ballerina/ballerina-core/src/interfaces/data-mapper.ts index 30f0c1a16c..6ccb86a8d4 100644 --- a/workspaces/ballerina/ballerina-core/src/interfaces/data-mapper.ts +++ b/workspaces/ballerina/ballerina-core/src/interfaces/data-mapper.ts @@ -57,9 +57,10 @@ export enum IntermediateClauseType { LET = "let", WHERE = "where", FROM = "from", - ORDER_BY = "order by", + ORDER_BY = "order-by", LIMIT = "limit", JOIN = "join", + GROUP_BY = "group-by" } export enum ResultClauseType { @@ -95,6 +96,7 @@ export interface IOType { defaultValue?: unknown; optional?: boolean; isFocused?: boolean; + isSeq?: boolean; isRecursive?: boolean; isDeepNested?: boolean; ref?: string; @@ -141,6 +143,7 @@ export interface DMModel { triggerRefresh?: boolean; traversingRoot?: string; focusInputRootMap?: Record; + groupById?: string; } export interface ModelState { @@ -175,6 +178,7 @@ export interface IOTypeField { optional?: boolean; ref?: string; focusExpression?: string; + isSeq?: boolean; typeInfo?: TypeInfo; } @@ -192,7 +196,7 @@ export interface Query { output: string, inputs: string[]; diagnostics?: DMDiagnostic[]; - fromClause: FromClause; + fromClause: IntermediateClause; intermediateClauses?: IntermediateClause[]; resultClause: ResultClause; } diff --git a/workspaces/ballerina/ballerina-extension/src/rpc-managers/data-mapper/utils.ts b/workspaces/ballerina/ballerina-extension/src/rpc-managers/data-mapper/utils.ts index 36c87a3a3e..b3357ad269 100644 --- a/workspaces/ballerina/ballerina-extension/src/rpc-managers/data-mapper/utils.ts +++ b/workspaces/ballerina/ballerina-extension/src/rpc-managers/data-mapper/utils.ts @@ -19,9 +19,6 @@ import { CodeData, ELineRange, Flow, - AllDataMapperSourceRequest, - DataMapperSourceRequest, - DataMapperSourceResponse, NodePosition, ProjectStructureArtifactResponse, TextEdit, @@ -34,7 +31,8 @@ import { IORoot, ExpandModelOptions, ExpandedDMModel, - MACHINE_VIEW + MACHINE_VIEW, + IntermediateClauseType } from "@wso2/ballerina-core"; import { updateSourceCode, UpdateSourceCodeRequest } from "../../utils"; import { StateMachine, updateDataMapperView } from "../../stateMachine"; @@ -598,6 +596,9 @@ function processArray( let fieldId = generateFieldId(parentId, member.name); let isFocused = false; + let isGroupByIdUpdated = false; + const prevGroupById = model.groupById; + if (model.focusInputs) { const focusMember = model.focusInputs[parentId]; if (focusMember) { @@ -605,9 +606,14 @@ function processArray( parentId = member.name; fieldId = member.name; isFocused = true; + model.focusInputRootMap[fieldId] = model.traversingRoot; - if (model.traversingRoot){ - model.focusInputRootMap[parentId] = model.traversingRoot; + if(member.isSeq && model.query!.fromClause.properties.name === fieldId){ + const groupByClause = model.query!.intermediateClauses?.find(clause => clause.type === IntermediateClauseType.GROUP_BY); + if(groupByClause){ + model.groupById = groupByClause.properties.name; + isGroupByIdUpdated = true; + } } } } @@ -625,6 +631,10 @@ function processArray( const typeSpecificProps = processTypeKind(member, parentId, model, visitedRefs); + if(isGroupByIdUpdated){ + model.groupById = prevGroupById; + } + return { ...ioType, ...typeSpecificProps @@ -718,13 +728,31 @@ function processTypeFields( if (!type.fields) { return []; } return type.fields.map(field => { - const fieldId = generateFieldId(parentId, field.name!); + let fieldId = generateFieldId(parentId, field.name!); + + let isFocused = false; + let isSeq = !!model.groupById; + if (model.focusInputs) { + const focusMember = model.focusInputs[fieldId]; + if (focusMember) { + field = focusMember; + fieldId = field.name; + isFocused = true; + model.focusInputRootMap[fieldId] = model.traversingRoot; + if (fieldId === model.groupById){ + isSeq = false; + } + } + } + const ioType: IOType = { id: fieldId, name: field.name, displayName: field.displayName, typeName: field.typeName, kind: field.kind, + ...(isFocused && { isFocused }), + ...(isSeq && { isSeq }), ...(field.optional !== undefined && { optional: field.optional }), ...(field.typeInfo && { typeInfo: field.typeInfo }) }; diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/DataMapper/DataMapperView.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/DataMapper/DataMapperView.tsx index a4ac82b876..804d1222d7 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/DataMapper/DataMapperView.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/DataMapper/DataMapperView.tsx @@ -43,7 +43,9 @@ import { MACHINE_VIEW, VisualizerLocation, DeleteClauseRequest, - IORoot + IORoot, + IntermediateClauseType, + TriggerKind } from "@wso2/ballerina-core"; import { CompletionItem, ProgressIndicator } from "@wso2/ui-toolkit"; import { useRpcContext } from "@wso2/ballerina-rpc-client"; @@ -368,9 +370,14 @@ export function DataMapperView(props: DataMapperProps) { targetField: targetField, index: index }); - return position; + if (position) { + return position; + } else { + throw new Error("Clause position not found"); + } } catch (error) { console.error(error); + return { line: 0, offset: 0 }; } } @@ -540,11 +547,45 @@ export function DataMapperView(props: DataMapperProps) { parentField.isDeepNested = false; } + const genUniqueName = async (name: string, viewId: string): Promise => { + const { property } = await rpcClient.getDataMapperRpcClient().getProperty({ + filePath, + codedata: viewState.codedata, + targetField: viewId + }) + + if (!property?.codedata?.lineRange?.startLine) { + console.error("Failed to get start line for generating unique name"); + return name; + } + + const completions = await rpcClient.getBIDiagramRpcClient().getDataMapperCompletions({ + filePath, + context: { + expression: "", + startLine: property.codedata.lineRange.startLine, + lineOffset: 0, + offset: 0, + codedata: viewState.codedata, + property: property + }, + completionContext: { + triggerKind: TriggerKind.INVOKED + } + }); + + let i = 2; + let uniqueName = name; + while (completions.some(c => c.insertText === uniqueName)) { + uniqueName = name + (i++); + } + return uniqueName; + }; const onDMClose = () => { onClose ? onClose() : rpcClient.getVisualizerRpcClient()?.goBack(); - } + }; const onDMRefresh = async () => { try { @@ -573,7 +614,7 @@ export function DataMapperView(props: DataMapperProps) { }; rpcClient.getVisualizerRpcClient().openView({ type: EVENT_TYPE.OPEN_VIEW, location: context }); - } + }; useEffect(() => { @@ -621,7 +662,7 @@ export function DataMapperView(props: DataMapperProps) { property: property }, completionContext: { - triggerKind: triggerCharacter ? 2 : 1, + triggerKind: triggerCharacter ? TriggerKind.TRIGGER_CHARACTER : TriggerKind.INVOKED, triggerCharacter: triggerCharacter as TriggerCharacter } }); @@ -708,6 +749,7 @@ export function DataMapperView(props: DataMapperProps) { mapWithTransformFn={mapWithTransformFn} goToFunction={goToFunction} enrichChildFields={enrichChildFields} + genUniqueName={genUniqueName} undoRedoGroup={undoRedoGroup} expressionBar={{ completions: filteredCompletions, @@ -727,7 +769,13 @@ export function DataMapperView(props: DataMapperProps) { }; const getModelSignature = (model: DMModel | ExpandedDMModel): ModelSignature => ({ - inputs: [...model.inputs.map(i => i.name), ...(model.query?.inputs || [])], + inputs: [...model.inputs.map(i => i.name), + ...(model.query?.inputs || []), + ...(model.query?.intermediateClauses + ?.filter((clause) => (clause.type === IntermediateClauseType.LET || clause.type === IntermediateClauseType.GROUP_BY)) + .map(clause => `${clause.properties.type} ${clause.properties.name} ${clause.properties.expression}`) + || []) + ], output: model.output.name, subMappings: model.subMappings?.map(s => (s as IORoot | IOType).name) || [], refs: 'refs' in model ? JSON.stringify(model.refs) : '' diff --git a/workspaces/ballerina/data-mapper/src/components/DataMapper/DataMapperEditor.tsx b/workspaces/ballerina/data-mapper/src/components/DataMapper/DataMapperEditor.tsx index 69bb98736d..3e9e0118f7 100644 --- a/workspaces/ballerina/data-mapper/src/components/DataMapper/DataMapperEditor.tsx +++ b/workspaces/ballerina/data-mapper/src/components/DataMapper/DataMapperEditor.tsx @@ -144,6 +144,7 @@ export function DataMapperEditor(props: DataMapperEditorProps) { mapWithTransformFn, goToFunction, enrichChildFields, + genUniqueName, undoRedoGroup } = props; const { @@ -236,10 +237,12 @@ export function DataMapperEditor(props: DataMapperEditorProps) { convertToQuery, deleteMapping, deleteSubMapping, + addClauses, mapWithCustomFn, mapWithTransformFn, goToFunction, - enrichChildFields + enrichChildFields, + genUniqueName ); const ioNodeInitVisitor = new IONodeInitVisitor(context); @@ -364,6 +367,7 @@ export function DataMapperEditor(props: DataMapperEditorProps) { deleteClause={deleteClause} getClausePosition={getClausePosition} generateForm={generateForm} + genUniqueName={genUniqueName} /> )} diff --git a/workspaces/ballerina/data-mapper/src/components/DataMapper/SidePanel/QueryClauses/ClauseEditor.tsx b/workspaces/ballerina/data-mapper/src/components/DataMapper/SidePanel/QueryClauses/ClauseEditor.tsx index 9d0d819115..75a57a3709 100644 --- a/workspaces/ballerina/data-mapper/src/components/DataMapper/SidePanel/QueryClauses/ClauseEditor.tsx +++ b/workspaces/ballerina/data-mapper/src/components/DataMapper/SidePanel/QueryClauses/ClauseEditor.tsx @@ -42,12 +42,13 @@ export function ClauseEditor(props: ClauseEditorProps) { 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 }, - { content: "join", value: IntermediateClauseType.JOIN }, + { content: "Condition", value: IntermediateClauseType.WHERE }, + { content: "Local variable", value: IntermediateClauseType.LET }, + { content: "Sort by", value: IntermediateClauseType.ORDER_BY }, + { content: "Limit", value: IntermediateClauseType.LIMIT }, + { content: "From", value: IntermediateClauseType.FROM }, + { content: "Join", value: IntermediateClauseType.JOIN }, + { content: "Group by", value: IntermediateClauseType.GROUP_BY } ] const nameField: DMFormField = { @@ -76,11 +77,15 @@ export function ClauseEditor(props: ClauseEditorProps) { const expressionField: DMFormField = { key: "expression", - label: clauseType === IntermediateClauseType.JOIN ? "Join With Collection" : "Expression", + label: clauseType === IntermediateClauseType.JOIN ? "Join With Collection" : + clauseType === IntermediateClauseType.GROUP_BY ? "Grouping Key" : + "Expression", type: "EXPRESSION", optional: false, editable: true, - documentation: clauseType === IntermediateClauseType.JOIN ? "Collection to be joined" : "Enter the expression of the clause", + documentation: clauseType === IntermediateClauseType.JOIN ? "Collection to be joined" : + clauseType === IntermediateClauseType.GROUP_BY ? "Enter the grouping key expression" : + "Enter the expression of the clause", value: clauseProps?.expression ?? "", valueTypeConstraint: "Global", enabled: true, @@ -129,10 +134,6 @@ export function ClauseEditor(props: ClauseEditorProps) { type: clauseType as IntermediateClauseType, properties: data as IntermediateClauseProps }; - if (clauseType === IntermediateClauseType.JOIN) { - clause.properties.type = "var"; - clause.properties.isOuter = false; - } onSubmit(clause); } @@ -151,6 +152,8 @@ export function ClauseEditor(props: ClauseEditorProps) { return [expressionField, orderField]; case IntermediateClauseType.JOIN: return [expressionField, nameField, lhsExpressionField, rhsExpressionField]; + case IntermediateClauseType.GROUP_BY: + return [expressionField]; default: return [expressionField]; } diff --git a/workspaces/ballerina/data-mapper/src/components/DataMapper/SidePanel/QueryClauses/ClausesPanel.tsx b/workspaces/ballerina/data-mapper/src/components/DataMapper/SidePanel/QueryClauses/ClausesPanel.tsx index 52d329b3f0..4826a1af99 100644 --- a/workspaces/ballerina/data-mapper/src/components/DataMapper/SidePanel/QueryClauses/ClausesPanel.tsx +++ b/workspaces/ballerina/data-mapper/src/components/DataMapper/SidePanel/QueryClauses/ClausesPanel.tsx @@ -23,7 +23,7 @@ import { useDMQueryClausesPanelStore } from "../../../../store/store"; import { AddButton, ClauseItem } from "./ClauseItem"; import { ClauseEditor } from "./ClauseEditor"; import { ClauseItemListContainer } from "./styles"; -import { DMFormProps, IntermediateClause, LinePosition, Query } from "@wso2/ballerina-core"; +import { DMFormProps, IntermediateClause, IntermediateClauseType, LinePosition, Query } from "@wso2/ballerina-core"; export interface ClausesPanelProps { query: Query; @@ -32,12 +32,13 @@ export interface ClausesPanelProps { deleteClause: (targetField: string, index: number) => Promise; getClausePosition: (targetField: string, index: number) => Promise; generateForm: (formProps: DMFormProps) => JSX.Element; + genUniqueName: (name: string, viewId: string) => Promise; } export function ClausesPanel(props: ClausesPanelProps) { const { isQueryClausesPanelOpen, setIsQueryClausesPanelOpen } = useDMQueryClausesPanelStore(); const { clauseToAdd, setClauseToAdd } = useDMQueryClausesPanelStore.getState(); - const { query, targetField, addClauses, deleteClause, getClausePosition, generateForm } = props; + const { query, targetField, addClauses, deleteClause, getClausePosition, generateForm , genUniqueName} = props; const [adding, setAdding] = React.useState(); const [editing, setEditing] = React.useState(); @@ -46,8 +47,20 @@ export function ClausesPanel(props: ClausesPanelProps) { const clauses = query?.intermediateClauses || []; + const fillDefaults = async (clause: IntermediateClause) => { + const clauseType = clause.type; + if (clauseType === IntermediateClauseType.JOIN) { + clause.properties.type = "var"; + clause.properties.isOuter = false; + } else if (clauseType === IntermediateClauseType.GROUP_BY) { + clause.properties.type = "var"; + clause.properties.name = await genUniqueName(clause.properties.expression.split('.').pop(), targetField); + } + }; + const setClauses = async (clause: IntermediateClause, isNew: boolean, index: number) => { setSaving(index); + await fillDefaults(clause); await addClauses(clause, targetField, isNew, index); setSaving(undefined); } diff --git a/workspaces/ballerina/data-mapper/src/components/Diagram/Label/MappingOptionsWidget.tsx b/workspaces/ballerina/data-mapper/src/components/Diagram/Label/MappingOptionsWidget.tsx index 0ec3c959db..e1d0e6c0a1 100644 --- a/workspaces/ballerina/data-mapper/src/components/Diagram/Label/MappingOptionsWidget.tsx +++ b/workspaces/ballerina/data-mapper/src/components/Diagram/Label/MappingOptionsWidget.tsx @@ -17,7 +17,7 @@ */ // tslint:disable: jsx-no-multiline-js -import React from 'react'; +import React, { useMemo } from 'react'; import { ResultClauseType, TypeKind } from '@wso2/ballerina-core'; import { Codicon, Item, Menu, MenuItem, ProgressRing } from '@wso2/ui-toolkit'; @@ -25,7 +25,7 @@ import { css } from '@emotion/css'; import { MappingType } from '../Link'; import { ExpressionLabelModel } from './ExpressionLabelModel'; -import { createNewMapping, mapWithCustomFn, mapWithQuery, mapWithTransformFn } from '../utils/modification-utils'; +import { createNewMapping, mapSeqToX, mapWithCustomFn, mapWithQuery, mapWithTransformFn } from '../utils/modification-utils'; import classNames from 'classnames'; import { genArrayElementAccessSuffix } from '../utils/common-utils'; import { InputOutputPortModel } from '../Port'; @@ -97,148 +97,160 @@ export function MappingOptionsWidget(props: MappingOptionsWidgetProps) { const pendingMappingType = link.pendingMappingType; const [inProgress, setInProgress] = React.useState(false); - const wrapWithProgress = (onClick: () => Promise) => { - return async () => { - setInProgress(true); - await onClick(); + + const menuItems: Item[] = useMemo(() => { + const wrapWithProgress = (onClick: () => Promise) => { + return async () => { + setInProgress(true); + await onClick(); + } + }; + + const onClickMapDirectly = async () => { + await createNewMapping(link); } - }; - const onClickMapDirectly = async () => { - await createNewMapping(link); - } - - const onClickMapIndividualElements = async () => { - await mapWithQuery(link, ResultClauseType.SELECT, context); - }; - - const onClickMapArraysAccessSingleton = async () => { - await createNewMapping(link, (expr: string) => `${expr}${genArrayElementAccessSuffix(link)}`); - }; - - const onClickAggregateArray = async () => { - await mapWithQuery(link, ResultClauseType.COLLECT, context); - }; - - const onClickMapWithCustomFn = async () => { - await mapWithCustomFn(link, context); - }; - - const onClickMapWithTransformFn = async () => { - await mapWithTransformFn(link, context); - } - - const onClickMapWithAggregateFn = async (fn: string) => { - await createNewMapping(link, (expr: string) => `${fn}(${expr})`); - } - - const getItemElement = (id: string, label: string) => { - return ( -
- - {label} -
- ); - } - - const a2aMenuItems: Item[] = [ - { - id: "a2a-direct", - label: getItemElement("a2a-direct", "Map Input Array to Output Array"), - onClick: wrapWithProgress(onClickMapDirectly) - }, - { - id: "a2a-inner", - label: getItemElement("a2a-inner", "Map Array Elements Individually"), - onClick: wrapWithProgress(onClickMapIndividualElements) + const onClickMapIndividualElements = async () => { + await mapWithQuery(link, ResultClauseType.SELECT, context); + }; + + const onClickMapArraysAccessSingleton = async () => { + await createNewMapping(link, (expr: string) => `${expr}${genArrayElementAccessSuffix(link)}`); + }; + + const onClickAggregateArray = async () => { + await mapWithQuery(link, ResultClauseType.COLLECT, context); + }; + + const onClickMapWithCustomFn = async () => { + await mapWithCustomFn(link, context); + }; + + const onClickMapWithTransformFn = async () => { + await mapWithTransformFn(link, context); } - ]; - - const defaultMenuItems: Item[] = [ - { - id: "a2a-direct", - label: getItemElement("direct", "Map Anyway"), - onClick: wrapWithProgress(onClickMapDirectly) + + const onClickMapWithAggregateFn = async (fn: string) => { + await createNewMapping(link, (expr: string) => `${fn}(${expr})`); } - ]; - - const genAggregateItems = () => { - const aggregateFnsNumeric = ["sum", "avg", "min", "max"]; - const aggregateFnsString = ["string:'join"]; - const aggregateFnsCommon = ["first", "last"]; - - const sourcePort = link.getSourcePort() as InputOutputPortModel; - const sourceType = sourcePort.attributes.field.kind; - const aggregateFns = [...(isNumericType(sourceType) ? aggregateFnsNumeric : - getGenericTypeKind(sourceType) === TypeKind.String ? aggregateFnsString : - []), - ...aggregateFnsCommon]; - const a2sAggregateItems: Item[] = aggregateFns.map((fn) => ({ - id: `a2s-collect-${fn}`, - label: getItemElement(`a2s-collect-${fn}`, `Aggregate using ${fn}`), - onClick: wrapWithProgress(async () => await onClickMapWithAggregateFn(fn)) - })); - return a2sAggregateItems; - }; + const onClickMapSeqToPrimitive = async (fn: string) => { + await mapSeqToX(link, context, (expr: string) => `${fn}(${expr})`); + } - const genArrayToSingletonItems = (): Item[] => { - const a2sMenuItems: Item[] = [ + const getItemElement = (id: string, label: string) => { + return ( +
+ + {label} +
+ ); + } + + const a2aMenuItems: Item[] = [ + { + id: "a2a-direct", + label: getItemElement("a2a-direct", "Map Input Array to Output Array"), + onClick: wrapWithProgress(onClickMapDirectly) + }, { - id: "a2s-index", - label: getItemElement("a2s-index", "Extract Single Element from Array"), - onClick: wrapWithProgress(onClickMapArraysAccessSingleton) + id: "a2a-inner", + label: getItemElement("a2a-inner", "Map Array Elements Individually"), + onClick: wrapWithProgress(onClickMapIndividualElements) } - ]; - const sourcePort = link.getSourcePort() as InputOutputPortModel; - const targetPort = link.getTargetPort() as InputOutputPortModel; - const sourceMemberType = sourcePort.attributes.field.member?.kind; - const targetType = targetPort.attributes.field.kind; - - if (sourceMemberType === targetType && isPrimitive(sourceMemberType)) { - a2sMenuItems.push({ - id: "a2s-aggregate", - label: getItemElement("a2s-aggregate", "Aggregate and map"), - onClick: wrapWithProgress(onClickAggregateArray) - }); - } - - return a2sMenuItems; - }; - - const genMenuItems = (): Item[] => { - switch (pendingMappingType) { - case MappingType.ArrayToArray: - return a2aMenuItems; - case MappingType.ArrayToSingleton: - return genArrayToSingletonItems(); - case MappingType.ArrayToSingletonAggregate: - return genAggregateItems(); - default: - return defaultMenuItems; - } - }; - - const menuItems = genMenuItems(); - - if (pendingMappingType !== MappingType.ArrayToSingletonAggregate) { - menuItems.push({ - id: "a2a-a2s-custom-func", - label: getItemElement("a2a-a2s-custom-func", "Map Using Custom Function"), - onClick: wrapWithProgress(onClickMapWithCustomFn) - }); - if (pendingMappingType !== MappingType.ContainsUnions) { + + const defaultMenuItems: Item[] = [ + { + id: "a2a-direct", + label: getItemElement("direct", "Map Anyway"), + onClick: wrapWithProgress(onClickMapDirectly) + } + ]; + + const genAggregateItems = (onClick: (fn: string) => Promise) => { + const aggregateFnsNumeric = ["sum", "avg", "min", "max"]; + const aggregateFnsString = ["string:'join"]; + const aggregateFnsCommon = ["first", "last"]; + + const sourcePort = link.getSourcePort() as InputOutputPortModel; + const sourceType = sourcePort.attributes.field.kind; + + const aggregateFns = [...(isNumericType(sourceType) ? aggregateFnsNumeric : + getGenericTypeKind(sourceType) === TypeKind.String ? aggregateFnsString : + []), + ...aggregateFnsCommon]; + const a2sAggregateItems: Item[] = aggregateFns.map((fn) => ({ + id: `a2s-collect-${fn}`, + label: getItemElement(`a2s-collect-${fn}`, `Aggregate using ${fn}`), + onClick: wrapWithProgress(async () => await onClick(fn)) + })); + return a2sAggregateItems; + }; + + const genArrayToSingletonItems = (): Item[] => { + const a2sMenuItems: Item[] = [ + { + id: "a2s-index", + label: getItemElement("a2s-index", "Extract Single Element from Array"), + onClick: wrapWithProgress(onClickMapArraysAccessSingleton) + } + + ]; + const sourcePort = link.getSourcePort() as InputOutputPortModel; + const targetPort = link.getTargetPort() as InputOutputPortModel; + const sourceMemberType = sourcePort.attributes.field.member?.kind; + const targetType = targetPort.attributes.field.kind; + + if (sourceMemberType === targetType && isPrimitive(sourceMemberType)) { + a2sMenuItems.push({ + id: "a2s-aggregate", + label: getItemElement("a2s-aggregate", "Aggregate and map"), + onClick: wrapWithProgress(onClickAggregateArray) + }); + } + + return a2sMenuItems; + }; + + const genMenuItems = (): Item[] => { + switch (pendingMappingType) { + case MappingType.ArrayToArray: + return a2aMenuItems; + case MappingType.ArrayToSingleton: + return genArrayToSingletonItems(); + case MappingType.ArrayToSingletonAggregate: + return genAggregateItems(onClickMapWithAggregateFn); + case MappingType.SeqToPrimitive: + return genAggregateItems(onClickMapSeqToPrimitive); + default: + return defaultMenuItems; + } + }; + + const menuItems = genMenuItems(); + + if (pendingMappingType !== MappingType.ArrayToSingletonAggregate && + pendingMappingType !== MappingType.SeqToPrimitive && + pendingMappingType !== MappingType.SeqToArray) { menuItems.push({ - id: "a2a-a2s-transform-func", - label: getItemElement("a2a-a2s-transform-func", "Map Using Transform Function"), - onClick: wrapWithProgress(onClickMapWithTransformFn) + id: "a2a-a2s-custom-func", + label: getItemElement("a2a-a2s-custom-func", "Map Using Custom Function"), + onClick: wrapWithProgress(onClickMapWithCustomFn) }); + if (pendingMappingType !== MappingType.ContainsUnions) { + menuItems.push({ + id: "a2a-a2s-transform-func", + label: getItemElement("a2a-a2s-transform-func", "Map Using Transform Function"), + onClick: wrapWithProgress(onClickMapWithTransformFn) + }); + } } - } + return menuItems; + }, [pendingMappingType, link, context]); return (
diff --git a/workspaces/ballerina/data-mapper/src/components/Diagram/Link/DataMapperLink/DataMapperLink.ts b/workspaces/ballerina/data-mapper/src/components/Diagram/Link/DataMapperLink/DataMapperLink.ts index 1a2be0c0dc..50f122425d 100644 --- a/workspaces/ballerina/data-mapper/src/components/Diagram/Link/DataMapperLink/DataMapperLink.ts +++ b/workspaces/ballerina/data-mapper/src/components/Diagram/Link/DataMapperLink/DataMapperLink.ts @@ -31,6 +31,8 @@ export enum MappingType { ArrayJoin = "array-join", Incompatible = "incompatible", ContainsUnions = "contains-unions", + SeqToPrimitive = "seq-primitive", + SeqToArray = "seq-array", Default = undefined // This is for non-array mappings currently } diff --git a/workspaces/ballerina/data-mapper/src/components/Diagram/Port/model/InputOutputPortModel.ts b/workspaces/ballerina/data-mapper/src/components/Diagram/Port/model/InputOutputPortModel.ts index 348b189749..d412f1b6e8 100644 --- a/workspaces/ballerina/data-mapper/src/components/Diagram/Port/model/InputOutputPortModel.ts +++ b/workspaces/ballerina/data-mapper/src/components/Diagram/Port/model/InputOutputPortModel.ts @@ -20,8 +20,9 @@ import { IOType, Mapping } from "@wso2/ballerina-core"; import { DataMapperLinkModel, MappingType } from "../../Link"; import { IntermediatePortModel } from "../IntermediatePort"; -import { createNewMapping, mapWithJoin } from "../../utils/modification-utils"; +import { createNewMapping, mapSeqToX, mapWithJoin } from "../../utils/modification-utils"; import { getMappingType, isPendingMappingRequired } from "../../utils/common-utils"; +import { DataMapperNodeModel } from "../../Node/commons/DataMapperNode"; export interface InputOutputPortModelGenerics { PORT: InputOutputPortModel; @@ -80,6 +81,11 @@ export class InputOutputPortModel extends PortModel `[${expr}]`); + return; + } await createNewMapping(lm); }) diff --git a/workspaces/ballerina/data-mapper/src/components/Diagram/hooks/useDiagramModel.ts b/workspaces/ballerina/data-mapper/src/components/Diagram/hooks/useDiagramModel.ts index b91569cbfa..309f4e9576 100644 --- a/workspaces/ballerina/data-mapper/src/components/Diagram/hooks/useDiagramModel.ts +++ b/workspaces/ballerina/data-mapper/src/components/Diagram/hooks/useDiagramModel.ts @@ -26,7 +26,7 @@ import { useDMSearchStore } from "../../../store/store"; import { InputNode } from "../Node"; import { getErrorKind } from "../utils/common-utils"; import { OverlayLayerModel } from "../OverlayLayer/OverlayLayerModel"; -import { IOType } from "@wso2/ballerina-core"; +import { IntermediateClauseType, IOType } from "@wso2/ballerina-core"; import { useEffect } from "react"; export const useDiagramModel = ( @@ -48,6 +48,9 @@ export const useDiagramModel = ( const mappings = model?.mappings.map(mapping => mapping.output + ':' + mapping.expression).toString(); const subMappings = model?.subMappings?.map(mapping => (mapping as IOType).id).toString(); const queryIOs = model?.query ? model.query.inputs.toString() + ':' + model.query.output : ''; + const queryClauses = model?.query?.intermediateClauses + ?.filter(clause => clause.type === IntermediateClauseType.LET || clause.type === IntermediateClauseType.GROUP_BY) + .map(clause => clause.properties.name + ':' + clause.properties.expression).toString(); const collapsedFields = useDMCollapsedFieldsStore(state => state.fields); // Subscribe to collapsedFields const expandedFields = useDMExpandedFieldsStore(state => state.fields); // Subscribe to expandedFields const { inputSearch, outputSearch } = useDMSearchStore(); @@ -101,7 +104,8 @@ export const useDiagramModel = ( outputSearch, mappings, subMappings, - queryIOs + queryIOs, + queryClauses ], queryFn: genModel, networkMode: 'always', diff --git a/workspaces/ballerina/data-mapper/src/components/Diagram/utils/common-utils.ts b/workspaces/ballerina/data-mapper/src/components/Diagram/utils/common-utils.ts index 1a54644bae..5384e0e195 100644 --- a/workspaces/ballerina/data-mapper/src/components/Diagram/utils/common-utils.ts +++ b/workspaces/ballerina/data-mapper/src/components/Diagram/utils/common-utils.ts @@ -65,7 +65,7 @@ export function hasChildMappingsForInput(mappings: Mapping[], inputId: string): } export function isPendingMappingRequired(mappingType: MappingType): boolean { - return mappingType !== MappingType.Default; + return mappingType !== MappingType.Default && mappingType !== MappingType.SeqToArray; } export function getMappingType(sourcePort: PortModel, targetPort: PortModel): MappingType { @@ -95,6 +95,15 @@ export function getMappingType(sourcePort: PortModel, targetPort: PortModel): Ma if (sourceField.kind === TypeKind.Union || targetField.kind === TypeKind.Union) { return MappingType.ContainsUnions; } + + if (sourceField.isSeq) { + if (isPrimitive(targetField.kind)){ + return MappingType.SeqToPrimitive; + } + if (targetField.kind === TypeKind.Array) { + return MappingType.SeqToArray; + } + } const sourceDim = getDMTypeDim(sourceField); const targetDim = getDMTypeDim(targetField); diff --git a/workspaces/ballerina/data-mapper/src/components/Diagram/utils/modification-utils.ts b/workspaces/ballerina/data-mapper/src/components/Diagram/utils/modification-utils.ts index b234ba8abc..d211e18f87 100644 --- a/workspaces/ballerina/data-mapper/src/components/Diagram/utils/modification-utils.ts +++ b/workspaces/ballerina/data-mapper/src/components/Diagram/utils/modification-utils.ts @@ -176,6 +176,57 @@ export async function mapWithQuery(link: DataMapperLinkModel, clauseType: Result expandArrayFn(context, [input], output, viewId); } + +export async function mapSeqToArray(link: DataMapperLinkModel, context: IDataMapperContext){ + const sourcePort = link.getSourcePort(); + const targetPort = link.getTargetPort(); + if (!sourcePort || !targetPort) { + return; + } + + const sourcePortModel = sourcePort as InputOutputPortModel; + +} + +export async function mapSeqToX(link: DataMapperLinkModel, context: IDataMapperContext, modifier: (expr: string) => string){ + const sourcePort = link.getSourcePort(); + const targetPort = link.getTargetPort(); + if (!sourcePort || !targetPort) { + return; + } + + const sourcePortModel = sourcePort as InputOutputPortModel; + + if (!sourcePortModel.attributes.field.isFocused){ + + const lastView = context.views[context.views.length - 1]; + const viewId = lastView.targetField; + + const clause = { + type: IntermediateClauseType.LET, + properties: { + name: await context.genUniqueName(sourcePortModel.attributes.field.name, viewId), + type: "var", + expression: sourcePortModel.attributes.fieldFQN, + } + } + + const groupByClauseIndex = context.model.query?.intermediateClauses + ?.findIndex(c => c.type === IntermediateClauseType.GROUP_BY); + + const letClauseIndex = (groupByClauseIndex !== -1 && groupByClauseIndex !== undefined) ? groupByClauseIndex - 1 : -1; + + await context.addClauses(clause, viewId, true, letClauseIndex); + + sourcePortModel.attributes.fieldFQN = clause.properties.name; + sourcePortModel.attributes.optionalOmittedFieldFQN = clause.properties.name; + + } + + await createNewMapping(link, modifier); + +} + export function mapWithJoin(link: DataMapperLinkModel) { const sourcePort = link.getSourcePort(); @@ -201,7 +252,6 @@ export function mapWithJoin(link: DataMapperLinkModel) { setIsQueryClausesPanelOpen(true); } - export function buildInputAccessExpr(fieldFqn: string): string { // Regular expression to match either quoted strings or non-quoted strings with dots const regex = /"([^"]+)"|'([^"]+)'|([^".]+)/g; diff --git a/workspaces/ballerina/data-mapper/src/components/Diagram/utils/type-utils.ts b/workspaces/ballerina/data-mapper/src/components/Diagram/utils/type-utils.ts index 02c2be2ca6..a1d3fc16a8 100644 --- a/workspaces/ballerina/data-mapper/src/components/Diagram/utils/type-utils.ts +++ b/workspaces/ballerina/data-mapper/src/components/Diagram/utils/type-utils.ts @@ -28,6 +28,10 @@ export function getTypeName(fieldType: IOType): string { } let typeName = fieldType?.typeName || fieldType.kind; + + if (fieldType.isSeq) { + return `...${typeName}`; + } return typeName; } diff --git a/workspaces/ballerina/data-mapper/src/index.tsx b/workspaces/ballerina/data-mapper/src/index.tsx index 27e713079c..b73c5a647f 100644 --- a/workspaces/ballerina/data-mapper/src/index.tsx +++ b/workspaces/ballerina/data-mapper/src/index.tsx @@ -82,6 +82,7 @@ export interface DataMapperEditorProps { onEdit?: () => void; handleView: (viewId: string, isSubMapping?: boolean) => void; generateForm: (formProps: DMFormProps) => JSX.Element; + genUniqueName: (name: string, viewId: string) => Promise; undoRedoGroup: () => JSX.Element; } diff --git a/workspaces/ballerina/data-mapper/src/utils/DataMapperContext/DataMapperContext.ts b/workspaces/ballerina/data-mapper/src/utils/DataMapperContext/DataMapperContext.ts index 0ad7a314d4..504604e822 100644 --- a/workspaces/ballerina/data-mapper/src/utils/DataMapperContext/DataMapperContext.ts +++ b/workspaces/ballerina/data-mapper/src/utils/DataMapperContext/DataMapperContext.ts @@ -15,7 +15,7 @@ * specific language governing permissions and limitations * under the License. */ -import { FnMetadata, ExpandedDMModel, IOType, LineRange, Mapping, ResultClauseType } from "@wso2/ballerina-core"; +import { FnMetadata, ExpandedDMModel, IOType, LineRange, Mapping, ResultClauseType, IntermediateClause } from "@wso2/ballerina-core"; import { View } from "../../components/DataMapper/Views/DataMapperView"; export interface IDataMapperContext { @@ -28,10 +28,12 @@ export interface IDataMapperContext { convertToQuery: (mapping: Mapping, clauseType: ResultClauseType, viewId: string, name: string) => Promise; deleteMapping: (mapping: Mapping, viewId: string) => Promise; deleteSubMapping: (index: number, viewId: string) => Promise; + addClauses: (clause: IntermediateClause, targetField: string, isNew: boolean, index:number) => Promise; mapWithCustomFn: (mapping: Mapping, metadata: FnMetadata, viewId: string) => Promise; mapWithTransformFn: (mapping: Mapping, metadata: FnMetadata, viewId: string) => Promise; goToFunction: (functionRange: LineRange) => Promise; enrichChildFields: (parentField: IOType) => Promise; + genUniqueName: (name: string, viewId: string) => Promise; } export class DataMapperContext implements IDataMapperContext { @@ -46,9 +48,11 @@ export class DataMapperContext implements IDataMapperContext { public convertToQuery: (mapping: Mapping, clauseType: ResultClauseType, viewId: string, name: string) => Promise, public deleteMapping: (mapping: Mapping, viewId: string) => Promise, public deleteSubMapping: (index: number, viewId: string) => Promise, + public addClauses: (clause: IntermediateClause, targetField: string, isNew: boolean, index:number) => Promise, public mapWithCustomFn: (mapping: Mapping, metadata: FnMetadata, viewId: string) => Promise, public mapWithTransformFn: (mapping: Mapping, metadata: FnMetadata, viewId: string) => Promise, public goToFunction: (functionRange: LineRange) => Promise, - public enrichChildFields: (parentField: IOType) => Promise + public enrichChildFields: (parentField: IOType) => Promise, + public genUniqueName: (name: string, viewId: string) => Promise ){} }