diff --git a/apps/Standalone/src/dataMapperV1/app/DataMapperStandaloneDesignerV2.tsx b/apps/Standalone/src/dataMapperV1/app/DataMapperStandaloneDesignerV2.tsx index c5a0778a3a3..8247e263417 100644 --- a/apps/Standalone/src/dataMapperV1/app/DataMapperStandaloneDesignerV2.tsx +++ b/apps/Standalone/src/dataMapperV1/app/DataMapperStandaloneDesignerV2.tsx @@ -51,8 +51,8 @@ class DataMapperFileService implements IDataMapperFileService { public saveMapDefinitionCall = (dataMapDefinition: string, mapMetadata: string) => { if (this.verbose) { - console.log(`Saved definition: ${dataMapDefinition}`); - console.log(`Saved metadata: ${mapMetadata}`); + console.log('Saved definition: ', dataMapDefinition); + console.log('Saved metadata: ', mapMetadata); } }; @@ -104,8 +104,15 @@ export const DataMapperStandaloneDesignerV2 = () => { const isLightMode = theme === ThemeType.Light; return ( -
-
+
+
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} @@ -117,7 +124,15 @@ export const DataMapperStandaloneDesignerV2 = () => {
-
+
void; @@ -12,10 +9,7 @@ export class DataMapperFileService implements IDataMapperFileService { this.sendMsgToVsix = sendMsgToVsix; } - public saveMapDefinitionCall = ( - dataMapDefinition: string, - mapMetadata: string - ) => { + public saveMapDefinitionCall = (dataMapDefinition: string, mapMetadata: string) => { this.sendMsgToVsix({ command: ExtensionCommand.saveDataMapDefinition, data: dataMapDefinition, diff --git a/libs/data-mapper-v2/src/components/addSchema/styles.ts b/libs/data-mapper-v2/src/components/addSchema/styles.ts index a3bdfcde885..5e5f019b8f2 100644 --- a/libs/data-mapper-v2/src/components/addSchema/styles.ts +++ b/libs/data-mapper-v2/src/components/addSchema/styles.ts @@ -41,7 +41,7 @@ export const useStyles = makeStyles({ }, root: { display: 'flex', - height: '100vh', + height: '100%', }, fileSelectedDrawer: { backgroundColor: '#F6FAFE', diff --git a/libs/data-mapper-v2/src/components/addSchema/tree/RecursiveTree.tsx b/libs/data-mapper-v2/src/components/addSchema/tree/RecursiveTree.tsx new file mode 100644 index 00000000000..ca48a726ff6 --- /dev/null +++ b/libs/data-mapper-v2/src/components/addSchema/tree/RecursiveTree.tsx @@ -0,0 +1,94 @@ +import { Tree, TreeItem, TreeItemLayout, type TreeItemOpenChangeData, mergeClasses } from '@fluentui/react-components'; +import type { SchemaNodeExtended } from '@microsoft/logic-apps-shared'; +import { useCallback, useEffect, useRef } from 'react'; +import { useStyles } from './styles'; +import type { Node } from 'reactflow'; +import useNodePosition from './useNodePosition'; + +type RecursiveTreeProps = { + root: SchemaNodeExtended; + isLeftDirection: boolean; + openKeys: Set; + setOpenKeys: (openKeys: Set) => void; + flattenedScehmaMap: Record; + setUpdatedNode: (node: Node) => void; +}; + +const RecursiveTree = (props: RecursiveTreeProps) => { + const { root, isLeftDirection, openKeys, setOpenKeys, flattenedScehmaMap, setUpdatedNode } = props; + const { key } = root; + const nodeRef = useRef(null); + const styles = useStyles(); + + const nodePosition = useNodePosition({ + key: key, + openKeys: openKeys, + schemaMap: flattenedScehmaMap, + isLeftDirection: isLeftDirection, + nodeX: nodeRef.current?.getBoundingClientRect().x, + nodeY: nodeRef.current?.getBoundingClientRect().y, + }); + + const onOpenChange = useCallback( + (_e: any, data: TreeItemOpenChangeData) => { + const newOpenKeys = new Set(openKeys); + const value = data.value as string; + if (newOpenKeys.has(value)) { + newOpenKeys.delete(value); + } else { + newOpenKeys.add(value); + } + setOpenKeys(newOpenKeys); + }, + [openKeys, setOpenKeys] + ); + + useEffect(() => { + const nodeId = `reactflow_${isLeftDirection ? 'source' : 'target'}_${root.key}`; + + setUpdatedNode({ + ...{ + id: nodeId, + selectable: true, + data: { + ...root, + isLeftDirection: isLeftDirection, + }, + type: 'schemaNode', + position: nodePosition, + }, + }); + }, [isLeftDirection, nodePosition, root, setUpdatedNode]); + + if (root.children.length === 0) { + return ( + + {root.name} + + ); + } + + return ( + + + {root.name} + + + {root.children.map((child: SchemaNodeExtended, index: number) => ( + + + + ))} + + + ); +}; + +export default RecursiveTree; diff --git a/libs/data-mapper-v2/src/components/addSchema/tree/SchemaTree.tsx b/libs/data-mapper-v2/src/components/addSchema/tree/SchemaTree.tsx index ed62a70f95f..54efba3e11d 100644 --- a/libs/data-mapper-v2/src/components/addSchema/tree/SchemaTree.tsx +++ b/libs/data-mapper-v2/src/components/addSchema/tree/SchemaTree.tsx @@ -1,9 +1,14 @@ -import { type SchemaExtended, SchemaType, type SchemaNodeExtended, equals } from '@microsoft/logic-apps-shared'; -import { Tree, TreeItem, TreeItemLayout, mergeClasses, type TreeItemOpenChangeData } from '@fluentui/react-components'; +import { type SchemaExtended, SchemaType, equals } from '@microsoft/logic-apps-shared'; +import { Tree, mergeClasses } from '@fluentui/react-components'; import { useStyles } from './styles'; -import { useEffect, useState, useMemo } from 'react'; -import { TreeNode } from './TreeNode'; +import { useState, useMemo, useRef, useEffect, useCallback } from 'react'; import { useIntl } from 'react-intl'; +import RecursiveTree from './RecursiveTree'; +import { flattenSchemaNodeMap } from '../../../utils'; +import { type Node, applyNodeChanges, type NodeChange } from 'reactflow'; +import type { AppDispatch, RootState } from '../../../core/state/Store'; +import { useDispatch, useSelector } from 'react-redux'; +import { updateReactFlowNodes } from '../../../core/state/DataMapSlice'; export type SchemaTreeProps = { schemaType?: SchemaType; @@ -12,10 +17,21 @@ export type SchemaTreeProps = { export const SchemaTree = (props: SchemaTreeProps) => { const styles = useStyles(); - const { schemaType } = props; + const { + schemaType, + schema: { schemaTreeRoot }, + } = props; + const isLeftDirection = useMemo(() => equals(schemaType, SchemaType.Source), [schemaType]); - const [openKeys, setOpenKeys] = useState>({}); + const [openKeys, setOpenKeys] = useState>(new Set()); + const [updatedNodes, setUpdatedNodes] = useState>({}); + const intl = useIntl(); + const dispatch = useDispatch(); + const treeRef = useRef(null); + const flattenedScehmaMap = useMemo(() => flattenSchemaNodeMap(schemaTreeRoot), [schemaTreeRoot]); + const totalNodes = useMemo(() => Object.keys(flattenedScehmaMap).length, [flattenedScehmaMap]); + const { nodes } = useSelector((state: RootState) => state.dataMap.present.curDataMapOperation); const treeAriaLabel = intl.formatMessage({ defaultMessage: 'Schema tree', @@ -23,57 +39,66 @@ export const SchemaTree = (props: SchemaTreeProps) => { description: 'tree showing schema nodes', }); - const onOpenTreeItem = (_event: any, data: TreeItemOpenChangeData) => { - setOpenKeys((prev) => ({ - ...prev, - [data.value]: !prev[data.value], - })); - }; + const setUpdatedNode = useCallback( + (node: Node) => { + setUpdatedNodes((prev) => ({ ...prev, [node.id]: node })); + }, + [setUpdatedNodes] + ); useEffect(() => { - const openKeys: Record = {}; - - const setDefaultState = (root: SchemaNodeExtended) => { - if (root.children.length > 0) { - openKeys[root.key] = true; + const keys = Object.keys(updatedNodes); + if (keys.length === totalNodes) { + const currentNodesMap: Record = {}; + for (const node of nodes) { + currentNodesMap[node.id] = node; } - for (const child of root.children) { - setDefaultState(child); - } - }; + const nodeChanges: NodeChange[] = []; + for (const key of keys) { + const updatedNode = updatedNodes[key]; + const currentNode = currentNodesMap[key]; - setDefaultState(props.schema.schemaTreeRoot); - setOpenKeys(openKeys); - }, [props.schema]); + if (updatedNode.position.x < 0 && updatedNode.position.y < 0) { + if (currentNode) { + nodeChanges.push({ id: key, type: 'remove' }); + } + } else if (!currentNode) { + nodeChanges.push({ type: 'add', item: updatedNode }); + } else if (currentNode.position.x !== updatedNode.position.x && currentNode.position.y !== updatedNode.position.y) { + nodeChanges.push({ + id: key, + type: 'position', + position: updatedNode.position, + }); + } + } - const displaySchemaTree = (root: SchemaNodeExtended) => { - if (root.children.length === 0) { - return ; + if (nodeChanges.length > 0) { + const newNodes = applyNodeChanges(nodeChanges, nodes); + setUpdatedNodes({}); + dispatch(updateReactFlowNodes(newNodes)); + } } + }, [nodes, updatedNodes, totalNodes, dispatch]); - return ( - - - {root.name} - - - {root.children.map((child: SchemaNodeExtended, index: number) => ( - {displaySchemaTree(child)} - ))} - - - ); - }; - - return props.schema.schemaTreeRoot ? ( + useEffect(() => { + setOpenKeys(new Set(Object.keys(flattenedScehmaMap).filter((key) => flattenedScehmaMap[key].children.length > 0))); + }, [flattenedScehmaMap, setOpenKeys]); + return schemaTreeRoot ? ( - {displaySchemaTree(props.schema.schemaTreeRoot)} + - ) : ( - <> - ); + ) : null; }; diff --git a/libs/data-mapper-v2/src/components/addSchema/tree/TreeNode.tsx b/libs/data-mapper-v2/src/components/addSchema/tree/TreeNode.tsx deleted file mode 100644 index 85133239560..00000000000 --- a/libs/data-mapper-v2/src/components/addSchema/tree/TreeNode.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { TreeItem, type TreeItemOpenChangeEvent, type TreeItemOpenChangeData, TreeItemLayout } from '@fluentui/react-components'; -import { useEffect, useRef, useCallback, useContext, useMemo } from 'react'; -import useIsInViewport from './UseInViewport.hook'; -import { useDispatch } from 'react-redux'; -import type { AppDispatch } from '../../../core/state/Store'; -import { SchemaType, type SchemaNodeExtended } from '@microsoft/logic-apps-shared'; -import { DataMapperWrappedContext } from '../../../core'; -import { useStyles } from './styles'; -import { updateReactFlowNode } from '../../../core/state/DataMapSlice'; - -export type SchemaNodeReactFlowDataProps = SchemaNodeExtended & { - isLeftDirection: boolean; - connectionX: number; - id: string; - isConnected?: boolean; -}; - -export type TreeNodeProps = { - isLeftDirection: boolean; - text: string; - id: string; - isHovered: boolean; - isAdded: boolean; - node: SchemaNodeExtended; -}; - -export const TreeNode = (props: TreeNodeProps) => { - const { isLeftDirection, id, node } = props; - const divRef = useRef(null); - const isInViewPort = useIsInViewport(divRef); - const dispatch = useDispatch(); - const styles = useStyles(); - const dataMapperContext = useContext(DataMapperWrappedContext); - - const nodeId = useMemo(() => `${isLeftDirection ? SchemaType.Source : SchemaType.Target}-${id}`, [id, isLeftDirection]); - const addNodeToFlow = useCallback(() => { - if (divRef?.current && dataMapperContext?.canvasRef?.current) { - const divRect = divRef.current.getBoundingClientRect(); - const canvasRect = dataMapperContext?.canvasRef.current.getBoundingClientRect(); - dispatch( - updateReactFlowNode({ - node: { - id: nodeId, - selectable: true, - data: { - ...node, - isLeftDirection: isLeftDirection, - connectionX: isLeftDirection ? canvasRect.left : canvasRect.right, - id: nodeId, - }, - type: 'schemaNode', - position: { - x: divRect.x - canvasRect.left, - y: divRect.y - canvasRect.y - 10, - }, - }, - }) - ); - } - }, [isLeftDirection, divRef, nodeId, node, dispatch, dataMapperContext?.canvasRef]); - - const removeNodeFromFlow = useCallback(() => { - dispatch( - updateReactFlowNode({ - node: { - id: nodeId, - selectable: true, - hidden: true, - data: node, - position: { x: 0, y: 0 }, - }, - removeNode: true, - }) - ); - }, [nodeId, node, dispatch]); - - const onOpenChange = useCallback( - (_event: TreeItemOpenChangeEvent, data: TreeItemOpenChangeData) => { - if (data.open && isInViewPort) { - addNodeToFlow(); - return; - } - - if (!data.open) { - removeNodeFromFlow(); - } - }, - [isInViewPort, addNodeToFlow, removeNodeFromFlow] - ); - - useEffect(() => { - if (!divRef?.current || !dataMapperContext?.canvasRef?.current || !isInViewPort) { - removeNodeFromFlow(); - } else { - addNodeToFlow(); - } - }, [divRef, isInViewPort, addNodeToFlow, removeNodeFromFlow, dataMapperContext?.canvasRef]); - return ( - { - onOpenChange(event, data); - }} - > - -
- - - ); -}; diff --git a/libs/data-mapper-v2/src/components/addSchema/tree/UseInViewport.hook.ts b/libs/data-mapper-v2/src/components/addSchema/tree/UseInViewport.hook.ts deleted file mode 100644 index e530b8c16e9..00000000000 --- a/libs/data-mapper-v2/src/components/addSchema/tree/UseInViewport.hook.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { type MutableRefObject, useEffect, useState, useMemo } from 'react'; - -const useIsInViewport = (ref: MutableRefObject) => { - const [isIntersecting, setIsIntersecting] = useState(false); - - const observer = useMemo(() => new IntersectionObserver(([entry]) => setIsIntersecting(entry.isIntersecting)), []); - - useEffect(() => { - if (ref?.current) { - observer.observe(ref.current); - } - - return () => { - observer.disconnect(); - }; - }, [ref, observer]); - - return isIntersecting; -}; - -export default useIsInViewport; diff --git a/libs/data-mapper-v2/src/components/addSchema/tree/useNodePosition.ts b/libs/data-mapper-v2/src/components/addSchema/tree/useNodePosition.ts new file mode 100644 index 00000000000..80132620bad --- /dev/null +++ b/libs/data-mapper-v2/src/components/addSchema/tree/useNodePosition.ts @@ -0,0 +1,48 @@ +import { useState, useEffect, useContext } from 'react'; +import type { XYPosition } from 'reactflow'; +import type { SchemaNodeExtended } from '@microsoft/logic-apps-shared'; +import { DataMapperWrappedContext } from '../../../core'; + +type NodePositionProps = { + key: string; + openKeys: Set; + schemaMap: Record; + isLeftDirection: boolean; + nodeX?: number; + nodeY?: number; +}; + +function isTreeNodeHidden(schemaMap: Record, openKeys: Set, key?: string) { + if (!key) { + return false; + } + + if (!openKeys.has(key)) { + return true; + } + + return isTreeNodeHidden(schemaMap, openKeys, schemaMap[key]?.parentKey); +} + +const useNodePosition = (props: NodePositionProps) => { + const { schemaMap, openKeys, key, isLeftDirection, nodeY = -1 } = props; + const [position, setPosition] = useState({ x: -1, y: -1 }); + + const { + canvasBounds: { y: canvasY = -1, width: canvasWidth = -1 } = {}, + } = useContext(DataMapperWrappedContext); + + useEffect(() => { + if (isTreeNodeHidden(schemaMap, openKeys, schemaMap[key]?.parentKey)) { + setPosition({ x: -1, y: -1 }); + } else if (canvasY >= 0 && nodeY >= 0 && canvasWidth >= 0) { + const x = isLeftDirection ? 0 : canvasWidth; + const y = nodeY - canvasY + 10; + setPosition({ x, y }); + } + }, [openKeys, schemaMap, canvasY, canvasWidth, nodeY, isLeftDirection, key]); + + return position; +}; + +export default useNodePosition; diff --git a/libs/data-mapper-v2/src/components/common/hooks/usePrevious.ts b/libs/data-mapper-v2/src/components/common/hooks/usePrevious.ts new file mode 100644 index 00000000000..a6e21fec685 --- /dev/null +++ b/libs/data-mapper-v2/src/components/common/hooks/usePrevious.ts @@ -0,0 +1,11 @@ +import { useEffect, useRef } from 'react'; + +const usePrevious = (value: T): T | undefined => { + const ref = useRef(); + useEffect(() => { + ref.current = value; + }); + return ref.current; +}; + +export default usePrevious; diff --git a/libs/data-mapper-v2/src/components/common/reactflow/SchemaNode.tsx b/libs/data-mapper-v2/src/components/common/reactflow/SchemaNode.tsx index d5ad22d1fad..c18c0a187f4 100644 --- a/libs/data-mapper-v2/src/components/common/reactflow/SchemaNode.tsx +++ b/libs/data-mapper-v2/src/components/common/reactflow/SchemaNode.tsx @@ -1,53 +1,29 @@ import { Handle, Position, useUpdateNodeInternals, type NodeProps } from 'reactflow'; -import type { SchemaNodeReactFlowDataProps } from 'components/addSchema/tree/TreeNode'; -import { Text, mergeClasses } from '@fluentui/react-components'; +import type { SchemaNodeReactFlowDataProps } from '../../../models/ReactFlow'; +import { mergeClasses } from '@fluentui/react-components'; import { useStyles } from './styles'; import { useRef, useEffect } from 'react'; const SchemaNode = (props: NodeProps) => { const divRef = useRef(null); const updateNodeInternals = useUpdateNodeInternals(); - const { data } = props; - const { name, isLeftDirection, connectionX, id, isConnected } = data; + const { data, id } = props; + const { isLeftDirection, isConnected } = data; const styles = useStyles(); useEffect(() => { - if (divRef.current) { - updateNodeInternals(id); - } - }, [divRef, id, updateNodeInternals]); + updateNodeInternals(id); + }, [id, updateNodeInternals]); return ( -
- {name} - {isLeftDirection ? ( - - ) : ( - - )} +
+
); }; diff --git a/libs/data-mapper-v2/src/components/common/reactflow/styles.ts b/libs/data-mapper-v2/src/components/common/reactflow/styles.ts index a950489f8d9..8ea4655155b 100644 --- a/libs/data-mapper-v2/src/components/common/reactflow/styles.ts +++ b/libs/data-mapper-v2/src/components/common/reactflow/styles.ts @@ -31,4 +31,9 @@ export const useStyles = makeStyles({ backgroundColor: '#C6DEEE', ...shorthands.border('1px', 'solid', '#C6DEEE'), }, + nodeWrapper: { + width: '10px', + height: '10px', + backgroundColor: 'transparent', + }, }); diff --git a/libs/data-mapper-v2/src/components/functionList/FunctionList.tsx b/libs/data-mapper-v2/src/components/functionList/FunctionList.tsx index aac2197b501..91488445a9d 100644 --- a/libs/data-mapper-v2/src/components/functionList/FunctionList.tsx +++ b/libs/data-mapper-v2/src/components/functionList/FunctionList.tsx @@ -127,13 +127,17 @@ export const FunctionList = () => { } return newFunctionListTree; - }, [functionData, searchTerm, inlineFunctionInputOutputKeys]); + }, [functionData, searchTerm, inlineFunctionInputOutputKeys, openItems]); - const treeItems = functionListTree.children.map((node) => { + const treeItems = functionListTree.children.map((node, index) => { return node.key.startsWith(functionCategoryItemKeyPrefix) ? ( - + ) : ( - + ); }); @@ -149,7 +153,13 @@ export const FunctionList = () => { /> - + {treeItems} diff --git a/libs/data-mapper-v2/src/core/DataMapperDesignerContext.tsx b/libs/data-mapper-v2/src/core/DataMapperDesignerContext.tsx index 5593605d875..6c085362006 100644 --- a/libs/data-mapper-v2/src/core/DataMapperDesignerContext.tsx +++ b/libs/data-mapper-v2/src/core/DataMapperDesignerContext.tsx @@ -1,8 +1,8 @@ -import { createContext, type MutableRefObject } from 'react'; +import { createContext } from 'react'; export interface DataMapperDesignerContext { readOnly?: boolean; - canvasRef?: MutableRefObject; + canvasBounds?: DOMRect; } -export const DataMapperWrappedContext = createContext(null); +export const DataMapperWrappedContext = createContext({}); diff --git a/libs/data-mapper-v2/src/core/state/DataMapSlice.ts b/libs/data-mapper-v2/src/core/state/DataMapSlice.ts index c1592e65cd6..f980b446589 100644 --- a/libs/data-mapper-v2/src/core/state/DataMapSlice.ts +++ b/libs/data-mapper-v2/src/core/state/DataMapSlice.ts @@ -13,7 +13,12 @@ import type { UnknownNode } from '../../utils/DataMap.Utils'; import { getParentId } from '../../utils/DataMap.Utils'; import { getConnectedSourceSchema, getFunctionLocationsForAllFunctions, isFunctionData } from '../../utils/Function.Utils'; import { LogService } from '../../utils/Logging.Utils'; -import { flattenSchemaIntoDictionary, flattenSchemaIntoSortArray, isSchemaNodeExtended } from '../../utils/Schema.Utils'; +import { + flattenSchemaIntoDictionary, + flattenSchemaIntoSortArray, + flattenSchemaNodeMap, + isSchemaNodeExtended, +} from '../../utils/Schema.Utils'; import type { FunctionMetadata, MapMetadata, SchemaExtended, SchemaNodeDictionary, SchemaNodeExtended } from '@microsoft/logic-apps-shared'; import { SchemaNodeProperty, SchemaType } from '@microsoft/logic-apps-shared'; import type { PayloadAction } from '@reduxjs/toolkit'; @@ -127,31 +132,49 @@ export const dataMapSlice = createSlice({ setInitialSchema: (state, action: PayloadAction) => { const flattenedSchema = flattenSchemaIntoDictionary(action.payload.schema, action.payload.schemaType); + const currentState = state.curDataMapOperation; if (action.payload.schemaType === SchemaType.Source) { const sourceSchemaSortArray = flattenSchemaIntoSortArray(action.payload.schema.schemaTreeRoot); + const sourceCurrentFlattenedSchemaMap = currentState.sourceSchema + ? flattenSchemaNodeMap(currentState.sourceSchema.schemaTreeRoot) + : {}; - state.curDataMapOperation.sourceSchema = action.payload.schema; - state.curDataMapOperation.flattenedSourceSchema = flattenedSchema; - state.curDataMapOperation.sourceSchemaOrdering = sourceSchemaSortArray; + currentState.sourceSchema = action.payload.schema; + currentState.flattenedSourceSchema = flattenedSchema; + currentState.sourceSchemaOrdering = sourceSchemaSortArray; state.pristineDataMap.sourceSchema = action.payload.schema; state.pristineDataMap.flattenedSourceSchema = flattenedSchema; state.pristineDataMap.sourceSchemaOrdering = sourceSchemaSortArray; + + // NOTE: Reset ReactFlow nodes to filter out source nodes + currentState.nodes = currentState.nodes.filter((node) => !sourceCurrentFlattenedSchemaMap[node.data.id]); } else { const targetSchemaSortArray = flattenSchemaIntoSortArray(action.payload.schema.schemaTreeRoot); - - state.curDataMapOperation.targetSchema = action.payload.schema; - state.curDataMapOperation.flattenedTargetSchema = flattenedSchema; - state.curDataMapOperation.targetSchemaOrdering = targetSchemaSortArray; - state.curDataMapOperation.currentTargetSchemaNode = undefined; + const targetCurrentFlattenedSchemaMap = currentState.targetSchema + ? flattenSchemaNodeMap(currentState.targetSchema.schemaTreeRoot) + : {}; + + currentState.targetSchema = action.payload.schema; + currentState.flattenedTargetSchema = flattenedSchema; + currentState.targetSchemaOrdering = targetSchemaSortArray; + currentState.currentTargetSchemaNode = undefined; state.pristineDataMap.targetSchema = action.payload.schema; state.pristineDataMap.flattenedTargetSchema = flattenedSchema; state.pristineDataMap.targetSchemaOrdering = targetSchemaSortArray; + + // NOTE: Reset ReactFlow nodes to filter out source nodes + currentState.nodes = currentState.nodes.filter((node) => !targetCurrentFlattenedSchemaMap[node.data.id]); } + // NOTE: Reset ReactFlow edges + currentState.edges = []; + if (state.curDataMapOperation.sourceSchema && state.curDataMapOperation.targetSchema) { - state.curDataMapOperation.currentTargetSchemaNode = state.curDataMapOperation.targetSchema.schemaTreeRoot; + currentState.currentTargetSchemaNode = state.curDataMapOperation.targetSchema.schemaTreeRoot; } + + state.curDataMapOperation = { ...currentState }; }, setInitialDataMap: (state, action: PayloadAction) => { @@ -197,14 +220,14 @@ export const dataMapSlice = createSlice({ if (action.payload.reactFlowSource.startsWith(SchemaType.Source)) { sourceNode = state.curDataMapOperation.flattenedSourceSchema[action.payload.reactFlowSource]; } else { - sourceNode = newState.functionNodes[action.payload.reactFlowSource].functionData; + sourceNode = newState.functionNodes[action.payload.reactFlowSource]?.functionData; } let destinationNode: UnknownNode; if (action.payload.reactFlowDestination.startsWith(SchemaType.Target)) { destinationNode = state.curDataMapOperation.flattenedTargetSchema[action.payload.reactFlowDestination]; } else { - destinationNode = newState.functionNodes[action.payload.reactFlowSource].functionData; + destinationNode = newState.functionNodes[action.payload.reactFlowSource]?.functionData; } addConnection(newState.dataMapConnections, action.payload, destinationNode, sourceNode); @@ -225,7 +248,10 @@ export const dataMapSlice = createSlice({ saveDataMap: ( state, - action: PayloadAction<{ sourceSchemaExtended: SchemaExtended | undefined; targetSchemaExtended: SchemaExtended | undefined }> + action: PayloadAction<{ + sourceSchemaExtended: SchemaExtended | undefined; + targetSchemaExtended: SchemaExtended | undefined; + }> ) => { const sourceSchemaExtended = action.payload.sourceSchemaExtended; const targetSchemaExtended = action.payload.targetSchemaExtended; diff --git a/libs/data-mapper-v2/src/models/ReactFlow.ts b/libs/data-mapper-v2/src/models/ReactFlow.ts index d60b31e719d..5ecac3275d8 100644 --- a/libs/data-mapper-v2/src/models/ReactFlow.ts +++ b/libs/data-mapper-v2/src/models/ReactFlow.ts @@ -1,6 +1,14 @@ +import type { SchemaNodeExtended } from '@microsoft/logic-apps-shared'; + export interface ViewportCoords { startX: number; endX: number; startY: number; endY: number; } + +export type SchemaNodeReactFlowDataProps = SchemaNodeExtended & { + isLeftDirection: boolean; + id: string; + isConnected?: boolean; +}; diff --git a/libs/data-mapper-v2/src/ui/DataMapperDesigner.tsx b/libs/data-mapper-v2/src/ui/DataMapperDesigner.tsx index 89b6aff4a65..de2d353a31c 100644 --- a/libs/data-mapper-v2/src/ui/DataMapperDesigner.tsx +++ b/libs/data-mapper-v2/src/ui/DataMapperDesigner.tsx @@ -1,9 +1,9 @@ import type { AppDispatch, RootState } from '../core/state/Store'; -import { useEffect, useMemo, useRef, useCallback } from 'react'; +import { useEffect, useMemo, useRef, useCallback, useState } from 'react'; import { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; import { useSelector, useDispatch } from 'react-redux'; -import type { Connection, CoordinateExtent, Node, Edge } from 'reactflow'; +import type { Connection, Node, Edge, ConnectionLineComponent } from 'reactflow'; import ReactFlow, { ReactFlowProvider, addEdge } from 'reactflow'; import { AddSchemaDrawer } from '../components/addSchema/AddSchemaPanel'; import { SchemaType } from '@microsoft/logic-apps-shared'; @@ -31,8 +31,17 @@ export const DataMapperDesigner = ({ fileService, readCurrentCustomXsltPathOptio useStaticStyles(); const styles = useStyles(); const ref = useRef(null); + const [canvasBounds, setCanvasBounds] = useState(); const dispatch = useDispatch(); + const updateCanvasBounds = useCallback(() => { + if (ref?.current) { + setCanvasBounds(ref.current.getBoundingClientRect()); + } + }, [ref]); + + const resizeObserver = useMemo(() => new ResizeObserver((_) => updateCanvasBounds()), [updateCanvasBounds]); + if (fileService) { InitDataMapperFileService(fileService); } @@ -54,20 +63,6 @@ export const DataMapperDesigner = ({ fileService, readCurrentCustomXsltPathOptio [] ); - const reactFlowExtent = useMemo(() => { - if (ref?.current) { - const rect = ref.current.getBoundingClientRect(); - if (rect) { - return [ - [0, 0], - [rect.width, rect.height], - ] as CoordinateExtent; - } - } - - return undefined; - }, [ref]); - const dispatchEdgesAndNodes = useCallback( (updatedEdges: Edge[], updatedNodes: Node[]) => { const allNodeIds = [...updatedEdges.map((edge) => edge.source), ...updatedEdges.map((edge) => edge.target)]; @@ -141,6 +136,17 @@ export const DataMapperDesigner = ({ fileService, readCurrentCustomXsltPathOptio [edges] ); + useEffect(() => { + if (ref?.current) { + resizeObserver.observe(ref.current); + updateCanvasBounds(); + } + + return () => { + resizeObserver.disconnect(); + }; + }, [ref, resizeObserver, updateCanvasBounds]); + useEffect(() => readCurrentCustomXsltPathOptions && readCurrentCustomXsltPathOptions(), [readCurrentCustomXsltPathOptions]); // NOTE: Putting this useEffect here for vis next to onSave @@ -153,7 +159,7 @@ export const DataMapperDesigner = ({ fileService, readCurrentCustomXsltPathOptio return ( - + {}} onTestClick={() => {}} />
@@ -187,8 +193,15 @@ export const DataMapperDesigner = ({ fileService, readCurrentCustomXsltPathOptio isValidConnection={isValidConnection} onConnect={onEdgeConnect} onEdgeUpdate={onEdgeUpdate} - connectionLineComponent={ConnectionLine} - translateExtent={reactFlowExtent} + connectionLineComponent={ConnectionLine as ConnectionLineComponent | undefined} + translateExtent={ + canvasBounds + ? [ + [0, 0], + [canvasBounds.right, canvasBounds.bottom], + ] + : undefined + } />
console.log(schema)} schemaType={SchemaType.Target} /> diff --git a/libs/data-mapper-v2/src/ui/styles.ts b/libs/data-mapper-v2/src/ui/styles.ts index 3d37eec3015..76a33a2c17e 100644 --- a/libs/data-mapper-v2/src/ui/styles.ts +++ b/libs/data-mapper-v2/src/ui/styles.ts @@ -4,7 +4,8 @@ import type { CSSProperties } from 'react'; export const useStyles = makeStyles({ dataMapperShell: { backgroundColor: tokens.colorNeutralBackground1, - height: '100%', + minHeight: 'calc(100vh - 50px)', + maxHeight: 'calc(100vh - 40px)', display: 'flex', ...shorthands.flex(1, 1, '1px'), }, diff --git a/libs/data-mapper-v2/src/utils/Function.Utils.ts b/libs/data-mapper-v2/src/utils/Function.Utils.ts index ffa6df53591..fa7ac5b4d2c 100644 --- a/libs/data-mapper-v2/src/utils/Function.Utils.ts +++ b/libs/data-mapper-v2/src/utils/Function.Utils.ts @@ -61,7 +61,8 @@ export const findFunctionForFunctionName = (nodeKey: string, functions: Function export const findFunctionForKey = (nodeKey: string, functions: FunctionData[]): FunctionData | undefined => functions.find((functionData) => functionData.key === nodeKey); -export const isFunctionData = (node: SchemaNodeExtended | FunctionData): node is FunctionData => 'functionName' in node; +export const isFunctionData = (node: SchemaNodeExtended | FunctionData): node is FunctionData => + Object.keys(node ?? {}).includes('functionName'); export const getFunctionOutputValue = (inputValues: string[], functionName: string) => { if (!functionName) { @@ -157,7 +158,10 @@ export const getFunctionLocationsForAllFunctions = ( }); const combinedTargetNodes = targetNodesConnectedToFunction.concat(parentNodes); - functionNodes[connectionKey] = { functionData: func, functionLocations: combinedTargetNodes }; + functionNodes[connectionKey] = { + functionData: func, + functionLocations: combinedTargetNodes, + }; } } return functionNodes; diff --git a/libs/data-mapper-v2/src/utils/Schema.Utils.ts b/libs/data-mapper-v2/src/utils/Schema.Utils.ts index 095208c6c78..6b112bfc1ff 100644 --- a/libs/data-mapper-v2/src/utils/Schema.Utils.ts +++ b/libs/data-mapper-v2/src/utils/Schema.Utils.ts @@ -1,11 +1,11 @@ // import type { FilteredDataTypesDict } from '../components/tree/SchemaTreeSearchbar'; // import { arrayType } from '../components/tree/SchemaTreeSearchbar'; // import type { ITreeNode } from '../components/tree/Tree'; -import { mapNodeParams } from '../constants/MapDefinitionConstants'; -import { sourcePrefix, targetPrefix } from '../constants/ReactFlowConstants'; +import { mapNodeParams } from "../constants/MapDefinitionConstants"; +import { sourcePrefix, targetPrefix } from "../constants/ReactFlowConstants"; //import { getLoopTargetNodeWithJson } from '../mapDefinitions'; -import type { FunctionData } from '../models/Function'; -import { LogCategory, LogService } from './Logging.Utils'; +import type { FunctionData } from "../models/Function"; +import { LogCategory, LogService } from "./Logging.Utils"; import type { PathItem, DataMapSchema, @@ -13,17 +13,27 @@ import type { SchemaNode, SchemaNodeDictionary, SchemaNodeExtended, -} from '@microsoft/logic-apps-shared'; -import { NormalizedDataType, SchemaNodeProperty, SchemaType } from '@microsoft/logic-apps-shared'; - -export const convertSchemaToSchemaExtended = (schema: DataMapSchema): SchemaExtended => { +} from "@microsoft/logic-apps-shared"; +import { + NormalizedDataType, + SchemaNodeProperty, + SchemaType, +} from "@microsoft/logic-apps-shared"; + +export const convertSchemaToSchemaExtended = ( + schema: DataMapSchema +): SchemaExtended => { const extendedSchema: SchemaExtended = { ...schema, - schemaTreeRoot: convertSchemaNodeToSchemaNodeExtended(schema.schemaTreeRoot, undefined, []), + schemaTreeRoot: convertSchemaNodeToSchemaNodeExtended( + schema.schemaTreeRoot, + undefined, + [] + ), }; - LogService.log(LogCategory.SchemaUtils, 'convertSchemaToSchemaExtended', { - message: 'Schema converted', + LogService.log(LogCategory.SchemaUtils, "convertSchemaToSchemaExtended", { + message: "Schema converted", data: { schemaFileFormat: schema.type, largestNode: telemetryLargestNode(extendedSchema), @@ -38,9 +48,12 @@ export const convertSchemaToSchemaExtended = (schema: DataMapSchema): SchemaExte }; export const getFileNameAndPath = (fullPath: string): [string, string] => { - const normalizedPath = fullPath.replaceAll('\\', '/'); - const lastIndexOfSlash = normalizedPath.lastIndexOf('/'); - const fileName = lastIndexOfSlash !== -1 ? normalizedPath.slice(lastIndexOfSlash + 1, normalizedPath.length + 1) : normalizedPath; + const normalizedPath = fullPath.replaceAll("\\", "/"); + const lastIndexOfSlash = normalizedPath.lastIndexOf("/"); + const fileName = + lastIndexOfSlash !== -1 + ? normalizedPath.slice(lastIndexOfSlash + 1, normalizedPath.length + 1) + : normalizedPath; const filePath = normalizedPath.slice(0, lastIndexOfSlash + 1); return [fileName, filePath]; }; @@ -50,7 +63,9 @@ const convertSchemaNodeToSchemaNodeExtended = ( parentKey: string | undefined, parentPath: PathItem[] ): SchemaNodeExtended => { - const nodeProperties = parsePropertiesIntoNodeProperties(schemaNode.properties); + const nodeProperties = parsePropertiesIntoNodeProperties( + schemaNode.properties + ); const pathToRoot: PathItem[] = [ ...parentPath, { @@ -65,30 +80,48 @@ const convertSchemaNodeToSchemaNodeExtended = ( ...schemaNode, nodeProperties, children: schemaNode.children - ? schemaNode.children.map((child) => convertSchemaNodeToSchemaNodeExtended(child, schemaNode.key, pathToRoot)) + ? schemaNode.children.map((child) => + convertSchemaNodeToSchemaNodeExtended( + child, + schemaNode.key, + pathToRoot + ) + ) : [], pathToRoot: pathToRoot, parentKey, - arrayItemIndex: nodeProperties.find((prop) => prop === SchemaNodeProperty.ArrayItem) ? 0 : undefined, + arrayItemIndex: nodeProperties.find( + (prop) => prop === SchemaNodeProperty.ArrayItem + ) + ? 0 + : undefined, }; return extendedSchemaNode; }; // Exported for testing purposes -export const parsePropertiesIntoNodeProperties = (propertiesString: string): SchemaNodeProperty[] => { +export const parsePropertiesIntoNodeProperties = ( + propertiesString: string +): SchemaNodeProperty[] => { if (propertiesString) { - return propertiesString.split(',').map((propertyString) => { - return SchemaNodeProperty[propertyString.trim() as keyof typeof SchemaNodeProperty]; + return propertiesString.split(",").map((propertyString) => { + return SchemaNodeProperty[ + propertyString.trim() as keyof typeof SchemaNodeProperty + ]; }); } return []; }; -export const flattenSchemaIntoDictionary = (schema: SchemaExtended, schemaType: SchemaType): SchemaNodeDictionary => { +export const flattenSchemaIntoDictionary = ( + schema: SchemaExtended, + schemaType: SchemaType +): SchemaNodeDictionary => { const result: SchemaNodeDictionary = {}; - const idPrefix = schemaType === SchemaType.Source ? sourcePrefix : targetPrefix; + const idPrefix = + schemaType === SchemaType.Source ? sourcePrefix : targetPrefix; const schemaNodeArray = flattenSchemaNode(schema.schemaTreeRoot); schemaNodeArray.reduce((dict, node) => { @@ -100,18 +133,36 @@ export const flattenSchemaIntoDictionary = (schema: SchemaExtended, schemaType: return result; }; -export const flattenSchemaIntoSortArray = (schemaNode: SchemaNodeExtended): string[] => { +export const flattenSchemaIntoSortArray = ( + schemaNode: SchemaNodeExtended +): string[] => { return flattenSchemaNode(schemaNode).map((node) => node.key); }; -export const flattenSchemaNode = (schemaNode: SchemaNodeExtended): SchemaNodeExtended[] => { +export const flattenSchemaNode = ( + schemaNode: SchemaNodeExtended +): SchemaNodeExtended[] => { const result: SchemaNodeExtended[] = [schemaNode]; - const childArray = schemaNode.children.flatMap((childNode) => flattenSchemaNode(childNode)); + const childArray = schemaNode.children.flatMap((childNode) => + flattenSchemaNode(childNode) + ); return result.concat(childArray); }; -export const isLeafNode = (schemaNode: SchemaNodeExtended): boolean => schemaNode.children.length < 1; +export const flattenSchemaNodeMap = ( + schemaNode: SchemaNodeExtended +): Record => { + const flattenedSchema = flattenSchemaNode(schemaNode); + const result: Record = {}; + for (const node of flattenedSchema) { + result[node.key] = node; + } + return result; +}; + +export const isLeafNode = (schemaNode: SchemaNodeExtended): boolean => + schemaNode.children.length < 1; /** * Finds a node for a key, searching within a given schema structure @@ -129,15 +180,18 @@ export const findNodeForKey = ( let tempKey = nodeKey; if (tempKey.includes(mapNodeParams.for)) { const layeredArrayItemForRegex = new RegExp(/\$for\([^)]*(?:\/\*){2,}\)/g); - tempKey = nodeKey.replaceAll(layeredArrayItemForRegex, ''); + tempKey = nodeKey.replaceAll(layeredArrayItemForRegex, ""); const forRegex = new RegExp(/\$for\([^)]+\)/g); // ArrayItems will have an * instead of a key name // And that * is stripped out during serialization - tempKey = tempKey.replaceAll(forRegex, nodeKey.indexOf('*') !== -1 ? '*' : ''); + tempKey = tempKey.replaceAll( + forRegex, + nodeKey.indexOf("*") !== -1 ? "*" : "" + ); - while (tempKey.indexOf('//') !== -1) { - tempKey = tempKey.replaceAll('//', '/'); + while (tempKey.indexOf("//") !== -1) { + tempKey = tempKey.replaceAll("//", "/"); } } @@ -151,20 +205,23 @@ export const findNodeForKey = ( return result; } - let lastInstanceOfMultiLoop = tempKey.lastIndexOf('*/*'); + let lastInstanceOfMultiLoop = tempKey.lastIndexOf("*/*"); while (lastInstanceOfMultiLoop > -1 && !result) { const start = tempKey.substring(0, lastInstanceOfMultiLoop); const end = tempKey.substring(lastInstanceOfMultiLoop + 2); tempKey = start + end; result = searchChildrenNodeForKey(tempKey, schemaNode); - lastInstanceOfMultiLoop = tempKey.lastIndexOf('*/*'); + lastInstanceOfMultiLoop = tempKey.lastIndexOf("*/*"); } return result; }; -const searchChildrenNodeForKey = (key: string, schemaNode: SchemaNodeExtended): SchemaNodeExtended | undefined => { +const searchChildrenNodeForKey = ( + key: string, + schemaNode: SchemaNodeExtended +): SchemaNodeExtended | undefined => { if (schemaNode.key === key) { return schemaNode; } @@ -181,68 +238,23 @@ const searchChildrenNodeForKey = (key: string, schemaNode: SchemaNodeExtended): return result; }; -// Search key will be the node's key -// Returns nodes that include the search key in their node.key (while maintaining the tree/schema's structure) -// export const searchSchemaTreeFromRoot = ( -// schemaTreeRoot: ITreeNode, -// flattenedSchema: SchemaNodeDictionary, -// nodeKeySearchTerm: string, -// filteredDataTypes: FilteredDataTypesDict -// ): ITreeNode => { -// const fuseSchemaTreeSearchOptions = { -// includeScore: true, -// minMatchCharLength: 2, -// includeMatches: true, -// threshold: 0.4, -// keys: ['qName'], -// }; - -// // Fuzzy search against flattened schema tree to build a dictionary of matches -// const fuse = new Fuse(Object.values(flattenedSchema), fuseSchemaTreeSearchOptions); -// const matchedSchemaNodesDict: { [key: string]: true } = {}; - -// fuse.search(nodeKeySearchTerm).forEach((result) => { -// matchedSchemaNodesDict[result.item.key] = true; -// }); - -// // Recurse through schema tree, adding children that match the criteria -// const searchChildren = (result: ITreeNode[], node: ITreeNode) => { -// // NOTE: Type-cast (safely) node for second condition so Typescript sees all properties -// if ( -// (nodeKeySearchTerm.length >= fuseSchemaTreeSearchOptions.minMatchCharLength ? matchedSchemaNodesDict[node.key] : true) && -// (filteredDataTypes[(node as SchemaNodeExtended).type] || -// ((node as SchemaNodeExtended).nodeProperties.includes(SchemaNodeProperty.Repeating) && filteredDataTypes[arrayType])) -// ) { -// result.push({ ...node }); -// } else if (node.children && node.children.length > 0) { -// const childNodes = node.children.reduce(searchChildren, []); - -// if (childNodes.length) { -// result.push({ ...node, isExpanded: true, children: childNodes } as ITreeNode); -// } -// } - -// return result; -// }; - -// return { -// ...schemaTreeRoot, -// isExpanded: true, -// children: schemaTreeRoot.children ? schemaTreeRoot.children.reduce[]>(searchChildren, []) : [], -// }; -// }; - -export const isSchemaNodeExtended = (node: SchemaNodeExtended | FunctionData): node is SchemaNodeExtended => 'pathToRoot' in node; +export const isSchemaNodeExtended = ( + node: SchemaNodeExtended | FunctionData +): node is SchemaNodeExtended => Object.keys(node ?? {}).includes("pathToRoot"); export const isObjectType = (nodeType: NormalizedDataType): boolean => - nodeType === NormalizedDataType.Complex || nodeType === NormalizedDataType.Object; + nodeType === NormalizedDataType.Complex || + nodeType === NormalizedDataType.Object; export const telemetryLargestNode = (schema: SchemaExtended): number => { return Math.max(...maxProperties(schema.schemaTreeRoot)); }; const maxProperties = (schemaNode: SchemaNodeExtended): number[] => { - return [schemaNode.children.length, ...schemaNode.children.flatMap((childNode) => maxProperties(childNode))]; + return [ + schemaNode.children.length, + ...schemaNode.children.flatMap((childNode) => maxProperties(childNode)), + ]; }; export const telemetryDeepestNodeChild = (schema: SchemaExtended): number => { @@ -250,7 +262,10 @@ export const telemetryDeepestNodeChild = (schema: SchemaExtended): number => { }; const deepestNode = (schemaNode: SchemaNodeExtended): number[] => { - return [schemaNode.pathToRoot.length, ...schemaNode.children.flatMap((childNode) => maxProperties(childNode))]; + return [ + schemaNode.pathToRoot.length, + ...schemaNode.children.flatMap((childNode) => maxProperties(childNode)), + ]; }; export const telemetrySchemaNodeCount = (schema: SchemaExtended): number => { @@ -267,6 +282,6 @@ const nodeCount = (schemaNode: SchemaNodeExtended): number => { }; export const removePrefixFromNodeID = (nodeID: string): string => { - const splitID = nodeID.split('-'); + const splitID = nodeID.split("-"); return splitID[1]; -} +};