From 1a8329ecc9a8d8e43a64ba0e7824386e43c0e6cf Mon Sep 17 00:00:00 2001 From: Jaryt Date: Tue, 29 Nov 2022 20:29:05 -0500 Subject: [PATCH 1/2] test: adding ghost --- src/components/flow/ConnectionLine.tsx | 7 +- src/components/flow/Flow.tsx | 89 ++++++++++++++++++++++---- src/components/flow/GhostNode.tsx | 48 ++++++++++++++ src/components/flow/JobNode.tsx | 8 +-- src/state/FlowStore.tsx | 3 + src/state/Store.tsx | 17 ----- 6 files changed, 136 insertions(+), 36 deletions(-) create mode 100644 src/components/flow/GhostNode.tsx diff --git a/src/components/flow/ConnectionLine.tsx b/src/components/flow/ConnectionLine.tsx index e2c0c9c..65612f7 100644 --- a/src/components/flow/ConnectionLine.tsx +++ b/src/components/flow/ConnectionLine.tsx @@ -1,14 +1,17 @@ +import { debug } from 'console'; import { ConnectionLineComponentProps, getBezierPath, MarkerType } from 'reactflow'; import { useStoreState } from '../../state/Hooks'; function ConnectionLine({ fromX, fromY, toX, toY, connectionLineStyle, fromNode, fromHandle }: ConnectionLineComponentProps) { const stepDist = 20; - const realStartX = fromX + (fromNode?.width || 2) / 2; + const realStartX = fromX - 4; const isSource = fromHandle?.position == 'right'; useStoreState((state) => state.mode); const valid = isSource ? toX >= realStartX + stepDist : toX <= realStartX + stepDist; const color = '#76CDFF'; + console.log(fromX, realStartX, fromNode) + const edgePath = valid ? getSteppedPath(realStartX, fromY, toX, toY, stepDist) : `M${realStartX},${fromY} ${realStartX + stepDist},${fromY}`; return ( @@ -26,7 +29,7 @@ function ConnectionLine({ fromX, fromY, toX, toY, connectionLineStyle, fromNode, - + {valid && } ); diff --git a/src/components/flow/Flow.tsx b/src/components/flow/Flow.tsx index ed78aa8..e63b3ae 100644 --- a/src/components/flow/Flow.tsx +++ b/src/components/flow/Flow.tsx @@ -2,7 +2,9 @@ import { useCallback, useEffect, useRef, useState } from "react"; import ReactFlow, { Background, useOnViewportChange, BackgroundVariant, ControlButton, Controls, MiniMap, Node, ReactFlowProvider, useEdgesState, useNodesState, useReactFlow, useStoreApi, useViewport, Viewport, XYPosition, applyNodeChanges, applyEdgeChanges, addEdge, Edge, Connection, ConnectionMode } from "reactflow"; import { FlowMode } from "../../state/FlowStore"; import { useStoreState } from "../../state/Hooks"; +import { DataModel } from "../../state/Store"; import ConnectionLine from "./ConnectionLine"; +import GhostNode from "./GhostNode"; import JobNode from "./JobNode"; export type FlowProps = { className?: string } @@ -10,6 +12,7 @@ export type FlowProps = { className?: string } const nodeTypes = { job: JobNode, + ghost: GhostNode, }; const initialNodes = [ @@ -17,7 +20,7 @@ const initialNodes = [ id: '1', type: 'job', data: { label: 'Node A' }, - position: { x: 250, y: 0 }, + position: { x: 0, y: 0 }, }, { id: '2', @@ -33,18 +36,80 @@ const initialNodes = [ }, ]; - const initialEdges = [{ id: 'e1-2', source: '1', target: '2', label: 'updatable edge' }]; - const MIN_DISTANCE = 1000; +const initialEdges = [{ id: 'e1-2', source: '1', target: '2', label: 'updatable edge' }]; +const MIN_DISTANCE = 1000; + +type FlowDropEvent = (position: XYPosition, dragging?: DataModel) => void; + +function useFlowOffset(func: FlowDropEvent, offset?: DOMRect, deps?: any[]) { + const { project } = useReactFlow(); + const dragging = useStoreState((state) => state.dragging); + + return useCallback((e: React.MouseEvent) => { +// console.log(dragging?.dataType?.dragTarget); + +// if (dragging?.dataType?.dragTarget !== 'workflow') { +// return; +// } + + const eventPos = project({ x: e.clientX, y: e.clientY - (offset?.y || 0)}); + + func(eventPos, dragging); + + e.preventDefault(); + }, deps ? [project, ...deps] : [project]); +} + +function useFlowDragAndDrop(offset?: DOMRect) { + const { addNodes, setNodes, deleteElements, getNodes, } = useReactFlow(); + const [lastPos, setPos] = useState(); + + const onDragEnter = useFlowOffset((position, data) => { + console.log(position) + addNodes({  id: 'ghost', position, type: 'ghost',data: ''}); + }, offset); // Upon entering the flow, create the drop ghost + const onDragOver = useFlowOffset((position, data) => { + if (lastPos) { + const dx = lastPos.x - position.x; + const dy = lastPos.y - position.y; + const dist = Math.abs(dx * dy); + + if (dist > 150) { + console.log(dist) + const nodes = getNodes(); + const changes = applyNodeChanges([ {  id: 'ghost', type: 'position', position, dragging: true } ], nodes); + setNodes(changes); + setPos(position); + } + } else { + setPos(position); + } + }, offset, [lastPos]); // While dragging, move the ghost + const onDragLeave = useFlowOffset((pos, data) => { + deleteElements({ nodes: [{ id: 'ghost' }]}) + console.log('leave', pos) + }, offset); // Upon drop and leave, remove the ghost + const onDrop = useFlowOffset((pos, data) => { + deleteElements({ nodes: [{ id: 'ghost' }]}) + console.log('end', pos) + }, offset); // Upon drop and leave, remove the ghost + + + return [onDragEnter, onDragOver, onDragLeave, onDrop] +} const Flow = (props: FlowProps) => { const store = useStoreApi(); - const dragging = useStoreState((state) => state.dragging); const viewport = useViewport(); + const { project, viewportInitialized } = useReactFlow(); const flowRef = useRef(null); - const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); + const nodeState = useNodesState(initialNodes); + const [nodes, setNodes, onNodesChange] = nodeState; const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); const mode = useStoreState((state) => state.mode); const onConnect = useCallback((params: Edge | Connection) => setEdges((eds) => addEdge(params, eds)), [setEdges]); + const flowRect = flowRef.current?.getBoundingClientRect(); + const [onDragEnter, onDragOver, onDragLeave, onDrop] = useFlowDragAndDrop(flowRect); return { nodes={nodes} edges={edges} ref={flowRef} + // onPaneMouseMove={(e) => { console.log(e.clientX, e.clientY)}} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} onConnect={onConnect} nodesDraggable={mode == FlowMode.MOVE} elementsSelectable={mode == FlowMode.SELECT} connectionLineComponent={ConnectionLine} + connectionLineStyle={ { zIndex: -10 }} snapToGrid nodeTypes={nodeTypes} className={props.className} - onDragOver={(e) => { - if (dragging?.dataType?.dragTarget === 'workflow') { - e.preventDefault(); - } - }} - onMouseMove={(event) => { - // getClosestNode({ x: event.clientX, y: event.clientY }); - }} + onDragOver={onDragOver} + onDragEnter={onDragEnter} + onDragLeaveCapture={onDragLeave} + onDrop={onDrop} onInit={(reactFlowInstance) => console.log('flow loaded:', reactFlowInstance)} > state.connectionNodeId; + +export default function GhostNode({ id, selected }: NodeProps) { + const connectionNodeId = useStore(connectionNodeIdSelector); + const mode = useStoreState((state) => state.mode); + + const isTarget = connectionNodeId && connectionNodeId !== id; + + const label = 'Ghost'; + const nodeBase = "p-2 pr-3 text-sm bg-white node flex flex-row text-black rounded-md border cursor-pointer" + const outline = selected ? "border-circle-blue" : " border-circle-gray-300" + + const Handles = useMemo(() => { + const connectMode = mode == FlowMode.CONNECT; + const handleBase = `${ connectMode ? '!w-full !h-full' : '!w-0 h-0'} !rounded opacity-50` + const notConnecting = connectionNodeId == null + + return ( + <> + + + + ) + + }, [mode, connectionNodeId]) + + return ( +
{ console.log('hehujasj') }}> + {label} + {Handles} +
+ ); +} diff --git a/src/components/flow/JobNode.tsx b/src/components/flow/JobNode.tsx index 97ad5d9..e0ed004 100644 --- a/src/components/flow/JobNode.tsx +++ b/src/components/flow/JobNode.tsx @@ -12,26 +12,26 @@ export default function JobNode({ id, selected }: NodeProps) { const isTarget = connectionNodeId && connectionNodeId !== id; - const label = isTarget ? 'Drop here' : 'Drag to connect'; + const label = 'Job Name'; const nodeBase = "p-2 pr-3 text-sm bg-white node flex flex-row text-black rounded-md border cursor-pointer" const outline = selected ? "border-circle-blue" : " border-circle-gray-300" const Handles = useMemo(() => { const connectMode = mode == FlowMode.CONNECT; - const handleBase = `${ connectMode ? '!w-full !h-full' : '!w-0 h-0'} !rounded opacity-0` + const handleBase = `${ connectMode ? '!w-full !h-full' : '!w-0 h-0'} !rounded opacity-50` const notConnecting = connectionNodeId == null return ( <> diff --git a/src/state/FlowStore.tsx b/src/state/FlowStore.tsx index 21a7aed..b2a942f 100644 --- a/src/state/FlowStore.tsx +++ b/src/state/FlowStore.tsx @@ -1,4 +1,5 @@ import { Action, action } from "easy-peasy"; +import { DataModel } from "./Store"; export enum FlowMode { SELECT, @@ -10,6 +11,8 @@ export enum FlowMode { export type FlowStoreModel = { mode: FlowMode; + /** Data being dragged from definition */ + dragging?: DataModel; } export type FlowActionsModel = { diff --git a/src/state/Store.tsx b/src/state/Store.tsx index 3aa79f9..9addd37 100644 --- a/src/state/Store.tsx +++ b/src/state/Store.tsx @@ -120,23 +120,6 @@ export type StoreModel = DefinitionsStoreModel & FlowStoreModel & { confirm?: ConfirmationModalModel; tooltip?: InfoToolTip; - /** Data being dragged from definition */ - dragging?: DataModel; - altAction?: boolean; - connecting?: { - intent: 'creating' | 'deleting'; - start?: { - ref?: MutableRefObject; - id: Connection; - name?: string; - }; - end?: { - id: Connection; - pos?: XYPosition; - ref?: MutableRefObject; - name?: string; - }; - }; /** Currently selected workflow pane index */ selectedWorkflowId: string; configError?: string; From 5c45f8e7911124153ce9d96d4bfb8025426053d7 Mon Sep 17 00:00:00 2001 From: Jaryt Date: Wed, 30 Nov 2022 17:31:46 -0500 Subject: [PATCH 2/2] refactor: Converted drag and drop functionality to hook --- src/App.tsx | 18 +- src/components/flow/Flow.tsx | 206 ------------------ src/{ => core}/components/atoms/AddButton.tsx | 2 +- src/{ => core}/components/atoms/Button.tsx | 0 src/{ => core}/components/atoms/Card.tsx | 0 .../components/atoms/ComponentInfo.tsx | 2 +- .../components/atoms/ConnectionLine.tsx | 0 .../components/atoms/Definition.tsx | 20 +- src/{ => core}/components/atoms/Edge.tsx | 0 src/{ => core}/components/atoms/Empty.tsx | 0 src/{ => core}/components/atoms/Footer.tsx | 2 +- src/{ => core}/components/atoms/Info.tsx | 0 .../components/atoms/OpenConfig.tsx | 2 +- src/{ => core}/components/atoms/Select.tsx | 2 +- src/{ => core}/components/atoms/Toast.tsx | 0 src/{ => core}/components/atoms/Tooltip.tsx | 0 .../components/atoms/WorkflowSelector.tsx | 2 +- .../atoms/form/AdjacentStepListItem.tsx | 0 .../atoms/form/ExecutorProperty.tsx | 2 +- .../atoms/form/InspectorProperty.tsx | 0 .../components/atoms/form/ListProperty.tsx | 0 .../components/atoms/form/MatrixProperty.tsx | 0 .../components/atoms/form/StepListItem.tsx | 0 .../components/atoms/nodes/JobNode.tsx | 0 .../atoms/summaries/CommandSummary.tsx | 0 .../atoms/summaries/ExecutorSummary.tsx | 0 .../components/atoms/summaries/JobSummary.tsx | 0 .../atoms/summaries/ParameterSummary.tsx | 0 .../components/containers/BreadCrumbs.tsx | 0 .../components/containers/CollapsibleList.tsx | 0 .../containers/ConfirmationModal.tsx | 2 +- .../containers/DefinitionsContainer.tsx | 2 +- .../containers/DropdownContainer.tsx | 0 .../components/containers/ExternalLinks.tsx | 2 +- .../containers/FilterPreviewContainer.tsx | 0 .../components/containers/GuideContainer.tsx | 0 .../components/containers/HeaderMenu.tsx | 2 +- .../components/containers/KBarList.tsx | 0 .../containers/OrbImportsContainer.tsx | 4 +- .../containers/ParamListContainer.tsx | 0 .../containers/ParametersContainer.tsx | 6 +- .../components/containers/PreviewToolbox.tsx | 0 .../containers/WorkflowContainer.tsx | 0 .../containers/inspector/CommandInspector.tsx | 2 +- .../inspector/ExecutorInspector.tsx | 0 .../containers/inspector/JobInspector.tsx | 4 +- .../inspector/ParameterInspector.tsx | 0 .../inspector/subtypes/CommandSubtypes.tsx | 0 .../inspector/subtypes/ExecutorSubtypes.tsx | 2 +- .../inspector/subtypes/ParameterSubtypes.tsx | 2 +- .../components/menus/SubTypeMenu.tsx | 0 .../components/menus/TabbedMenu.tsx | 0 .../menus/definitions/DefinitionsMenu.tsx | 6 +- .../definitions/InspectorDefinitionMenu.tsx | 11 +- .../menus/definitions/OrbDefinitionsMenu.tsx | 4 +- .../menus/definitions/OrbImportMenu.tsx | 2 +- .../menus/definitions/StepDefinitionMenu.tsx | 0 .../definitions/subtypes/ExecutorTypePage.tsx | 0 .../subtypes/ParameterTypePage.tsx | 2 +- .../definitions/subtypes/StepTypePage.tsx | 0 .../menus/stage/StagedFilterMenu.tsx | 0 .../components/menus/stage/StagedJobMenu.tsx | 2 +- .../components/panes/EditorPane.tsx | 4 +- .../components/panes/NavigationPane.tsx | 2 +- .../components/panes/WorkflowsPane.tsx | 6 +- src/{ => core}/icons/IconProps.tsx | 0 .../icons/components/CommandIcon.tsx | 0 .../icons/components/ExecutorIcon.tsx | 0 src/{ => core}/icons/components/JobIcon.tsx | 0 .../icons/components/JobOnHoldIcon.tsx | 0 src/{ => core}/icons/components/OrbIcon.tsx | 0 .../icons/components/ParameterIcon.tsx | 0 .../icons/components/WorkflowIcon.tsx | 0 src/{ => core}/icons/ui/AddIcon.tsx | 0 src/{ => core}/icons/ui/BranchIcon.tsx | 0 .../icons/ui/BreadCrumbArrowIcon.tsx | 0 src/{ => core}/icons/ui/CircleCI.tsx | 0 src/{ => core}/icons/ui/CopyIcon.tsx | 0 src/{ => core}/icons/ui/DeleteItemIcon.tsx | 0 src/{ => core}/icons/ui/DragItemIcon.tsx | 0 src/{ => core}/icons/ui/EditIcon.tsx | 0 src/{ => core}/icons/ui/ExpandIcon.tsx | 0 src/{ => core}/icons/ui/FilterIcon.tsx | 0 src/{ => core}/icons/ui/InfoIcon.tsx | 0 src/{ => core}/icons/ui/Loading.tsx | 0 src/{ => core}/icons/ui/Logo.tsx | 0 src/{ => core}/icons/ui/MinusIcon.tsx | 0 src/{ => core}/icons/ui/MoreIcon.tsx | 0 src/{ => core}/icons/ui/NewWindowIcon.tsx | 0 src/{ => core}/icons/ui/OpenIcon.tsx | 0 src/{ => core}/icons/ui/PlusIcon.tsx | 0 src/{ => core}/icons/ui/Switch.tsx | 0 src/{ => core}/icons/ui/TagIcon.tsx | 0 .../icons/ui/ToolTipPointerIcon.tsx | 0 src/{ => core}/state/DefinitionStore.tsx | 14 +- src/{ => core}/state/Hooks.tsx | 0 src/{ => core}/state/Store.tsx | 19 +- .../components}/ConnectionLine.tsx | 2 +- src/flow/components/Flow.tsx | 116 ++++++++++ .../flow => flow/components}/FlowTools.tsx | 12 +- .../flow => flow/components}/GhostNode.tsx | 4 +- .../flow => flow/components}/JobNode.tsx | 16 +- src/flow/hooks/useFlowDnD.ts | 71 ++++++ src/flow/hooks/useFlowOffset.ts | 18 ++ src/{ => flow}/state/FlowStore.tsx | 8 +- src/{ => flow}/util/FlowUtil.tsx | 0 src/hooks/useMapping.tsx | 5 - src/mappings/GenerableMapping.tsx | 4 +- src/mappings/InspectableMapping.tsx | 16 +- src/mappings/NodeMapping.tsx | 22 ++ src/mappings/components/CommandMapping.tsx | 10 +- src/mappings/components/ExecutorMapping.tsx | 14 +- src/mappings/components/JobMapping.tsx | 69 +++--- src/mappings/components/ParameterMapping.tsx | 12 +- src/mappings/components/WorkflowMapping.tsx | 4 +- 115 files changed, 386 insertions(+), 375 deletions(-) delete mode 100644 src/components/flow/Flow.tsx rename src/{ => core}/components/atoms/AddButton.tsx (96%) rename src/{ => core}/components/atoms/Button.tsx (100%) rename src/{ => core}/components/atoms/Card.tsx (100%) rename src/{ => core}/components/atoms/ComponentInfo.tsx (93%) rename src/{ => core}/components/atoms/ConnectionLine.tsx (100%) rename src/{ => core}/components/atoms/Definition.tsx (83%) rename src/{ => core}/components/atoms/Edge.tsx (100%) rename src/{ => core}/components/atoms/Empty.tsx (100%) rename src/{ => core}/components/atoms/Footer.tsx (91%) rename src/{ => core}/components/atoms/Info.tsx (100%) rename src/{ => core}/components/atoms/OpenConfig.tsx (96%) rename src/{ => core}/components/atoms/Select.tsx (98%) rename src/{ => core}/components/atoms/Toast.tsx (100%) rename src/{ => core}/components/atoms/Tooltip.tsx (100%) rename src/{ => core}/components/atoms/WorkflowSelector.tsx (95%) rename src/{ => core}/components/atoms/form/AdjacentStepListItem.tsx (100%) rename src/{ => core}/components/atoms/form/ExecutorProperty.tsx (98%) rename src/{ => core}/components/atoms/form/InspectorProperty.tsx (100%) rename src/{ => core}/components/atoms/form/ListProperty.tsx (100%) rename src/{ => core}/components/atoms/form/MatrixProperty.tsx (100%) rename src/{ => core}/components/atoms/form/StepListItem.tsx (100%) rename src/{ => core}/components/atoms/nodes/JobNode.tsx (100%) rename src/{ => core}/components/atoms/summaries/CommandSummary.tsx (100%) rename src/{ => core}/components/atoms/summaries/ExecutorSummary.tsx (100%) rename src/{ => core}/components/atoms/summaries/JobSummary.tsx (100%) rename src/{ => core}/components/atoms/summaries/ParameterSummary.tsx (100%) rename src/{ => core}/components/containers/BreadCrumbs.tsx (100%) rename src/{ => core}/components/containers/CollapsibleList.tsx (100%) rename src/{ => core}/components/containers/ConfirmationModal.tsx (98%) rename src/{ => core}/components/containers/DefinitionsContainer.tsx (98%) rename src/{ => core}/components/containers/DropdownContainer.tsx (100%) rename src/{ => core}/components/containers/ExternalLinks.tsx (93%) rename src/{ => core}/components/containers/FilterPreviewContainer.tsx (100%) rename src/{ => core}/components/containers/GuideContainer.tsx (100%) rename src/{ => core}/components/containers/HeaderMenu.tsx (83%) rename src/{ => core}/components/containers/KBarList.tsx (100%) rename src/{ => core}/components/containers/OrbImportsContainer.tsx (99%) rename src/{ => core}/components/containers/ParamListContainer.tsx (100%) rename src/{ => core}/components/containers/ParametersContainer.tsx (96%) rename src/{ => core}/components/containers/PreviewToolbox.tsx (100%) rename src/{ => core}/components/containers/WorkflowContainer.tsx (100%) rename src/{ => core}/components/containers/inspector/CommandInspector.tsx (96%) rename src/{ => core}/components/containers/inspector/ExecutorInspector.tsx (100%) rename src/{ => core}/components/containers/inspector/JobInspector.tsx (97%) rename src/{ => core}/components/containers/inspector/ParameterInspector.tsx (100%) rename src/{ => core}/components/containers/inspector/subtypes/CommandSubtypes.tsx (100%) rename src/{ => core}/components/containers/inspector/subtypes/ExecutorSubtypes.tsx (96%) rename src/{ => core}/components/containers/inspector/subtypes/ParameterSubtypes.tsx (98%) rename src/{ => core}/components/menus/SubTypeMenu.tsx (100%) rename src/{ => core}/components/menus/TabbedMenu.tsx (100%) rename src/{ => core}/components/menus/definitions/DefinitionsMenu.tsx (94%) rename src/{ => core}/components/menus/definitions/InspectorDefinitionMenu.tsx (96%) rename src/{ => core}/components/menus/definitions/OrbDefinitionsMenu.tsx (98%) rename src/{ => core}/components/menus/definitions/OrbImportMenu.tsx (99%) rename src/{ => core}/components/menus/definitions/StepDefinitionMenu.tsx (100%) rename src/{ => core}/components/menus/definitions/subtypes/ExecutorTypePage.tsx (100%) rename src/{ => core}/components/menus/definitions/subtypes/ParameterTypePage.tsx (97%) rename src/{ => core}/components/menus/definitions/subtypes/StepTypePage.tsx (100%) rename src/{ => core}/components/menus/stage/StagedFilterMenu.tsx (100%) rename src/{ => core}/components/menus/stage/StagedJobMenu.tsx (99%) rename src/{ => core}/components/panes/EditorPane.tsx (97%) rename src/{ => core}/components/panes/NavigationPane.tsx (93%) rename src/{ => core}/components/panes/WorkflowsPane.tsx (93%) rename src/{ => core}/icons/IconProps.tsx (100%) rename src/{ => core}/icons/components/CommandIcon.tsx (100%) rename src/{ => core}/icons/components/ExecutorIcon.tsx (100%) rename src/{ => core}/icons/components/JobIcon.tsx (100%) rename src/{ => core}/icons/components/JobOnHoldIcon.tsx (100%) rename src/{ => core}/icons/components/OrbIcon.tsx (100%) rename src/{ => core}/icons/components/ParameterIcon.tsx (100%) rename src/{ => core}/icons/components/WorkflowIcon.tsx (100%) rename src/{ => core}/icons/ui/AddIcon.tsx (100%) rename src/{ => core}/icons/ui/BranchIcon.tsx (100%) rename src/{ => core}/icons/ui/BreadCrumbArrowIcon.tsx (100%) rename src/{ => core}/icons/ui/CircleCI.tsx (100%) rename src/{ => core}/icons/ui/CopyIcon.tsx (100%) rename src/{ => core}/icons/ui/DeleteItemIcon.tsx (100%) rename src/{ => core}/icons/ui/DragItemIcon.tsx (100%) rename src/{ => core}/icons/ui/EditIcon.tsx (100%) rename src/{ => core}/icons/ui/ExpandIcon.tsx (100%) rename src/{ => core}/icons/ui/FilterIcon.tsx (100%) rename src/{ => core}/icons/ui/InfoIcon.tsx (100%) rename src/{ => core}/icons/ui/Loading.tsx (100%) rename src/{ => core}/icons/ui/Logo.tsx (100%) rename src/{ => core}/icons/ui/MinusIcon.tsx (100%) rename src/{ => core}/icons/ui/MoreIcon.tsx (100%) rename src/{ => core}/icons/ui/NewWindowIcon.tsx (100%) rename src/{ => core}/icons/ui/OpenIcon.tsx (100%) rename src/{ => core}/icons/ui/PlusIcon.tsx (100%) rename src/{ => core}/icons/ui/Switch.tsx (100%) rename src/{ => core}/icons/ui/TagIcon.tsx (100%) rename src/{ => core}/icons/ui/ToolTipPointerIcon.tsx (100%) rename src/{ => core}/state/DefinitionStore.tsx (97%) rename src/{ => core}/state/Hooks.tsx (100%) rename src/{ => core}/state/Store.tsx (98%) rename src/{components/flow => flow/components}/ConnectionLine.tsx (96%) create mode 100644 src/flow/components/Flow.tsx rename src/{components/flow => flow/components}/FlowTools.tsx (62%) rename src/{components/flow => flow/components}/GhostNode.tsx (94%) rename src/{components/flow => flow/components}/JobNode.tsx (76%) create mode 100644 src/flow/hooks/useFlowDnD.ts create mode 100644 src/flow/hooks/useFlowOffset.ts rename src/{ => flow}/state/FlowStore.tsx (78%) rename src/{ => flow}/util/FlowUtil.tsx (100%) delete mode 100644 src/hooks/useMapping.tsx create mode 100644 src/mappings/NodeMapping.tsx diff --git a/src/App.tsx b/src/App.tsx index f6014e8..6d3756d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,16 +1,16 @@ import algoliasearch from 'algoliasearch'; import { createStore, StoreProvider } from 'easy-peasy'; import { useRef } from 'react'; -import Toast from './components/atoms/Toast'; -import ToolTip from './components/atoms/Tooltip'; -import ConfirmationModal from './components/containers/ConfirmationModal'; -import KBarList from './components/containers/KBarList'; -import EditorPane from './components/panes/EditorPane'; -import NavigationPane from './components/panes/NavigationPane'; -import WorkflowsPane from './components/panes/WorkflowsPane'; +import Toast from './core/components/atoms/Toast'; +import ToolTip from './core/components/atoms/Tooltip'; +import ConfirmationModal from './core/components/containers/ConfirmationModal'; +import KBarList from './core/components/containers/KBarList'; +import EditorPane from './core/components/panes/EditorPane'; +import NavigationPane from './core/components/panes/NavigationPane'; +import WorkflowsPane from './core/components/panes/WorkflowsPane'; import './index.css'; -import useWindowDimensions, { useStoreState } from './state/Hooks'; -import Store from './state/Store'; +import useWindowDimensions, { useStoreState } from './core/state/Hooks'; +import Store from './core/state/Store'; export const store = createStore(Store); export const inspectorWidth = 400; diff --git a/src/components/flow/Flow.tsx b/src/components/flow/Flow.tsx deleted file mode 100644 index e63b3ae..0000000 --- a/src/components/flow/Flow.tsx +++ /dev/null @@ -1,206 +0,0 @@ -import { useCallback, useEffect, useRef, useState } from "react"; -import ReactFlow, { Background, useOnViewportChange, BackgroundVariant, ControlButton, Controls, MiniMap, Node, ReactFlowProvider, useEdgesState, useNodesState, useReactFlow, useStoreApi, useViewport, Viewport, XYPosition, applyNodeChanges, applyEdgeChanges, addEdge, Edge, Connection, ConnectionMode } from "reactflow"; -import { FlowMode } from "../../state/FlowStore"; -import { useStoreState } from "../../state/Hooks"; -import { DataModel } from "../../state/Store"; -import ConnectionLine from "./ConnectionLine"; -import GhostNode from "./GhostNode"; -import JobNode from "./JobNode"; - -export type FlowProps = { className?: string } - - -const nodeTypes = { - job: JobNode, - ghost: GhostNode, -}; - -const initialNodes = [ - { - id: '1', - type: 'job', - data: { label: 'Node A' }, - position: { x: 0, y: 0 }, - }, - { - id: '2', - type: 'job', - data: { label: 'Node B' }, - position: { x: 100, y: 200 }, - }, - { - id: '3', - type: 'job', - data: { label: 'Node C' }, - position: { x: 350, y: 200 }, - }, - ]; - -const initialEdges = [{ id: 'e1-2', source: '1', target: '2', label: 'updatable edge' }]; -const MIN_DISTANCE = 1000; - -type FlowDropEvent = (position: XYPosition, dragging?: DataModel) => void; - -function useFlowOffset(func: FlowDropEvent, offset?: DOMRect, deps?: any[]) { - const { project } = useReactFlow(); - const dragging = useStoreState((state) => state.dragging); - - return useCallback((e: React.MouseEvent) => { -// console.log(dragging?.dataType?.dragTarget); - -// if (dragging?.dataType?.dragTarget !== 'workflow') { -// return; -// } - - const eventPos = project({ x: e.clientX, y: e.clientY - (offset?.y || 0)}); - - func(eventPos, dragging); - - e.preventDefault(); - }, deps ? [project, ...deps] : [project]); -} - -function useFlowDragAndDrop(offset?: DOMRect) { - const { addNodes, setNodes, deleteElements, getNodes, } = useReactFlow(); - const [lastPos, setPos] = useState(); - - const onDragEnter = useFlowOffset((position, data) => { - console.log(position) - addNodes({  id: 'ghost', position, type: 'ghost',data: ''}); - }, offset); // Upon entering the flow, create the drop ghost - const onDragOver = useFlowOffset((position, data) => { - if (lastPos) { - const dx = lastPos.x - position.x; - const dy = lastPos.y - position.y; - const dist = Math.abs(dx * dy); - - if (dist > 150) { - console.log(dist) - const nodes = getNodes(); - const changes = applyNodeChanges([ {  id: 'ghost', type: 'position', position, dragging: true } ], nodes); - setNodes(changes); - setPos(position); - } - } else { - setPos(position); - } - }, offset, [lastPos]); // While dragging, move the ghost - const onDragLeave = useFlowOffset((pos, data) => { - deleteElements({ nodes: [{ id: 'ghost' }]}) - console.log('leave', pos) - }, offset); // Upon drop and leave, remove the ghost - const onDrop = useFlowOffset((pos, data) => { - deleteElements({ nodes: [{ id: 'ghost' }]}) - console.log('end', pos) - }, offset); // Upon drop and leave, remove the ghost - - - return [onDragEnter, onDragOver, onDragLeave, onDrop] -} - -const Flow = (props: FlowProps) => { - const store = useStoreApi(); - const viewport = useViewport(); - const { project, viewportInitialized } = useReactFlow(); - const flowRef = useRef(null); - const nodeState = useNodesState(initialNodes); - const [nodes, setNodes, onNodesChange] = nodeState; - const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); - const mode = useStoreState((state) => state.mode); - const onConnect = useCallback((params: Edge | Connection) => setEdges((eds) => addEdge(params, eds)), [setEdges]); - const flowRect = flowRef.current?.getBoundingClientRect(); - const [onDragEnter, onDragOver, onDragLeave, onDrop] = useFlowDragAndDrop(flowRect); - - - return { console.log(e.clientX, e.clientY)}} - onNodesChange={onNodesChange} - onEdgesChange={onEdgesChange} - onConnect={onConnect} - nodesDraggable={mode == FlowMode.MOVE} - elementsSelectable={mode == FlowMode.SELECT} - connectionLineComponent={ConnectionLine} - connectionLineStyle={ { zIndex: -10 }} - snapToGrid - nodeTypes={nodeTypes} - className={props.className} - onDragOver={onDragOver} - onDragEnter={onDragEnter} - onDragLeaveCapture={onDragLeave} - onDrop={onDrop} - onInit={(reactFlowInstance) => console.log('flow loaded:', reactFlowInstance)} - > - - - - -} - -const GraphWrapper = (props: FlowProps) => { - return - - -} - -export { GraphWrapper, Flow }; - // const onConnect = useCallback((params) => setEdges((els) => addEdge(params, els)), []); - - // const onNodesChange = useCallback( - // (changes) => setNodes((nds) => applyNodeChanges(changes, nds)), - // [setNodes] - // ); - // const onEdgesChange = useCallback( - // (changes) => setEdges((eds) => applyEdgeChanges(changes, eds)), - // [setEdges] - // ); - // const onConnect = useCallback( - // (connection) => setEdges((eds) => addEdge(connection, eds)), - // [setEdges] - // ); - - // const getClosestNode = useCallback((pos: XYPosition) => { - // const { nodeInternals } = store.getState(); - // const storeNodes = Array.from(nodeInternals.values()); - - // const closestNode = storeNodes.reduce<{ distance: number, node?: Node}>( - // (res, n) => { - // const boundingRect = flowRef.current?.getBoundingClientRect() || { x: 0, y: 0}; - // const mouseX = Math.abs((pos.x - boundingRect.x) * viewport.zoom + viewport.x); - // const mouseY = Math.abs((pos.y - boundingRect.y) * viewport.zoom + viewport.y) - // const dx = Math.abs(n.position.x) - mouseX; - // const dy = Math.abs(n.position.y) - mouseY; - // const d = Math.sqrt(dx * dx + dy * dy); - - // // console.log(pos.y - boundingRect.y, mouseY, viewport.y) - - // if (d < res.distance && d < MIN_DISTANCE) { - // res.distance = d; - // res.node = n; - // } - - // return res; - // }, - // { - // distance: Number.MAX_VALUE, - // node: undefined, - // } - // ); - - // if (!closestNode.node) { - // return null; - // } - - // // console.log(closestNode.node) - - // return closestNode.node; - // }, [viewport]); diff --git a/src/components/atoms/AddButton.tsx b/src/core/components/atoms/AddButton.tsx similarity index 96% rename from src/components/atoms/AddButton.tsx rename to src/core/components/atoms/AddButton.tsx index 47f9c7a..e2d7f55 100644 --- a/src/components/atoms/AddButton.tsx +++ b/src/core/components/atoms/AddButton.tsx @@ -7,7 +7,7 @@ const AddButton = (props: ButtonHTMLAttributes) => { type="button" {...props} className={ - `bg-circle-gray-300 transition-colors h-8 w-8 rounded ${props.className} + `bg-circle-gray-300 transition-colors h-8 w-8 rounded ${props.className} ${props.disabled ? 'opacity-50 cursor-default' : 'hover:bg-circle-gray-400 '}` } > diff --git a/src/components/atoms/Button.tsx b/src/core/components/atoms/Button.tsx similarity index 100% rename from src/components/atoms/Button.tsx rename to src/core/components/atoms/Button.tsx diff --git a/src/components/atoms/Card.tsx b/src/core/components/atoms/Card.tsx similarity index 100% rename from src/components/atoms/Card.tsx rename to src/core/components/atoms/Card.tsx diff --git a/src/components/atoms/ComponentInfo.tsx b/src/core/components/atoms/ComponentInfo.tsx similarity index 93% rename from src/components/atoms/ComponentInfo.tsx rename to src/core/components/atoms/ComponentInfo.tsx index 27cc6bf..8cc764e 100644 --- a/src/components/atoms/ComponentInfo.tsx +++ b/src/core/components/atoms/ComponentInfo.tsx @@ -1,6 +1,6 @@ import InspectableMapping, { GenerableInfoType, -} from '../../mappings/InspectableMapping'; +} from '../../../mappings/InspectableMapping'; const ComponentInfo = ({ type, diff --git a/src/components/atoms/ConnectionLine.tsx b/src/core/components/atoms/ConnectionLine.tsx similarity index 100% rename from src/components/atoms/ConnectionLine.tsx rename to src/core/components/atoms/ConnectionLine.tsx diff --git a/src/components/atoms/Definition.tsx b/src/core/components/atoms/Definition.tsx similarity index 83% rename from src/components/atoms/Definition.tsx rename to src/core/components/atoms/Definition.tsx index 24d26ae..c5e6e60 100644 --- a/src/components/atoms/Definition.tsx +++ b/src/core/components/atoms/Definition.tsx @@ -5,7 +5,7 @@ import { OrbImport, OrbRef, } from '@circleci/circleci-config-sdk/dist/src/lib/Orb'; -import InspectableMapping from '../../mappings/InspectableMapping'; +import InspectableMapping from '../../../mappings/InspectableMapping'; import { useStoreActions } from '../../state/Hooks'; import { InspectorDefinitionMenuNav } from '../menus/definitions/InspectorDefinitionMenu'; @@ -35,7 +35,7 @@ export const flattenGenerable = (data: Generable, nested?: boolean) => { )[0]; }; -const Definition = (props: { +const Definition = ({ data, ...props}: { data: Generable | OrbRef; type: InspectableMapping; index: number; @@ -47,27 +47,27 @@ const Definition = (props: { return ( ); }; diff --git a/src/components/atoms/Edge.tsx b/src/core/components/atoms/Edge.tsx similarity index 100% rename from src/components/atoms/Edge.tsx rename to src/core/components/atoms/Edge.tsx diff --git a/src/components/atoms/Empty.tsx b/src/core/components/atoms/Empty.tsx similarity index 100% rename from src/components/atoms/Empty.tsx rename to src/core/components/atoms/Empty.tsx diff --git a/src/components/atoms/Footer.tsx b/src/core/components/atoms/Footer.tsx similarity index 91% rename from src/components/atoms/Footer.tsx rename to src/core/components/atoms/Footer.tsx index ef1d469..b749af3 100644 --- a/src/components/atoms/Footer.tsx +++ b/src/core/components/atoms/Footer.tsx @@ -1,5 +1,5 @@ import { PropsWithChildren } from 'react'; -import { inspectorWidth } from '../../App'; +import { inspectorWidth } from '../../../App'; export const Footer = ({ children, diff --git a/src/components/atoms/Info.tsx b/src/core/components/atoms/Info.tsx similarity index 100% rename from src/components/atoms/Info.tsx rename to src/core/components/atoms/Info.tsx diff --git a/src/components/atoms/OpenConfig.tsx b/src/core/components/atoms/OpenConfig.tsx similarity index 96% rename from src/components/atoms/OpenConfig.tsx rename to src/core/components/atoms/OpenConfig.tsx index b94fc35..2a3b6f0 100644 --- a/src/components/atoms/OpenConfig.tsx +++ b/src/core/components/atoms/OpenConfig.tsx @@ -5,7 +5,7 @@ import { useStoreActions, useStoreState, } from '../../state/Hooks'; -import { Button } from '../atoms/Button'; +import { Button } from './Button'; export const OpenConfig = () => { const inputFile = useRef(null); diff --git a/src/components/atoms/Select.tsx b/src/core/components/atoms/Select.tsx similarity index 98% rename from src/components/atoms/Select.tsx rename to src/core/components/atoms/Select.tsx index afd1600..0b7ee26 100644 --- a/src/components/atoms/Select.tsx +++ b/src/core/components/atoms/Select.tsx @@ -61,7 +61,7 @@ const Select = (props: SelectProps) => { props.borderless ? 'border-transparent' : ' border-circle-gray-300 shadow-sm' - } px-2 hover:border-circle-gray-700 border + } px-2 hover:border-circle-gray-700 border ${props.className}`} >
diff --git a/src/components/atoms/Toast.tsx b/src/core/components/atoms/Toast.tsx similarity index 100% rename from src/components/atoms/Toast.tsx rename to src/core/components/atoms/Toast.tsx diff --git a/src/components/atoms/Tooltip.tsx b/src/core/components/atoms/Tooltip.tsx similarity index 100% rename from src/components/atoms/Tooltip.tsx rename to src/core/components/atoms/Tooltip.tsx diff --git a/src/components/atoms/WorkflowSelector.tsx b/src/core/components/atoms/WorkflowSelector.tsx similarity index 95% rename from src/components/atoms/WorkflowSelector.tsx rename to src/core/components/atoms/WorkflowSelector.tsx index 21118e4..ac48d17 100644 --- a/src/components/atoms/WorkflowSelector.tsx +++ b/src/core/components/atoms/WorkflowSelector.tsx @@ -1,7 +1,7 @@ import { v4 } from 'uuid'; import WorkflowIcon from '../../icons/components/WorkflowIcon'; import ExpandIcon from '../../icons/ui/ExpandIcon'; -import { WorkflowStage } from '../../mappings/components/WorkflowMapping'; +import { WorkflowStage } from '../../../mappings/components/WorkflowMapping'; import { useStoreActions, useStoreState } from '../../state/Hooks'; import DropdownContainer from '../containers/DropdownContainer'; diff --git a/src/components/atoms/form/AdjacentStepListItem.tsx b/src/core/components/atoms/form/AdjacentStepListItem.tsx similarity index 100% rename from src/components/atoms/form/AdjacentStepListItem.tsx rename to src/core/components/atoms/form/AdjacentStepListItem.tsx diff --git a/src/components/atoms/form/ExecutorProperty.tsx b/src/core/components/atoms/form/ExecutorProperty.tsx similarity index 98% rename from src/components/atoms/form/ExecutorProperty.tsx rename to src/core/components/atoms/form/ExecutorProperty.tsx index 82ef582..36cd539 100644 --- a/src/components/atoms/form/ExecutorProperty.tsx +++ b/src/core/components/atoms/form/ExecutorProperty.tsx @@ -31,7 +31,7 @@ export const ExecutorProperty = ({ label={label || "Executor"} as="select" name={`${name}.name`} - className="w-full" + className="w-full" {...props} dependent={(executorName: string) => { const splitName = executorName?.split('/'); diff --git a/src/components/atoms/form/InspectorProperty.tsx b/src/core/components/atoms/form/InspectorProperty.tsx similarity index 100% rename from src/components/atoms/form/InspectorProperty.tsx rename to src/core/components/atoms/form/InspectorProperty.tsx diff --git a/src/components/atoms/form/ListProperty.tsx b/src/core/components/atoms/form/ListProperty.tsx similarity index 100% rename from src/components/atoms/form/ListProperty.tsx rename to src/core/components/atoms/form/ListProperty.tsx diff --git a/src/components/atoms/form/MatrixProperty.tsx b/src/core/components/atoms/form/MatrixProperty.tsx similarity index 100% rename from src/components/atoms/form/MatrixProperty.tsx rename to src/core/components/atoms/form/MatrixProperty.tsx diff --git a/src/components/atoms/form/StepListItem.tsx b/src/core/components/atoms/form/StepListItem.tsx similarity index 100% rename from src/components/atoms/form/StepListItem.tsx rename to src/core/components/atoms/form/StepListItem.tsx diff --git a/src/components/atoms/nodes/JobNode.tsx b/src/core/components/atoms/nodes/JobNode.tsx similarity index 100% rename from src/components/atoms/nodes/JobNode.tsx rename to src/core/components/atoms/nodes/JobNode.tsx diff --git a/src/components/atoms/summaries/CommandSummary.tsx b/src/core/components/atoms/summaries/CommandSummary.tsx similarity index 100% rename from src/components/atoms/summaries/CommandSummary.tsx rename to src/core/components/atoms/summaries/CommandSummary.tsx diff --git a/src/components/atoms/summaries/ExecutorSummary.tsx b/src/core/components/atoms/summaries/ExecutorSummary.tsx similarity index 100% rename from src/components/atoms/summaries/ExecutorSummary.tsx rename to src/core/components/atoms/summaries/ExecutorSummary.tsx diff --git a/src/components/atoms/summaries/JobSummary.tsx b/src/core/components/atoms/summaries/JobSummary.tsx similarity index 100% rename from src/components/atoms/summaries/JobSummary.tsx rename to src/core/components/atoms/summaries/JobSummary.tsx diff --git a/src/components/atoms/summaries/ParameterSummary.tsx b/src/core/components/atoms/summaries/ParameterSummary.tsx similarity index 100% rename from src/components/atoms/summaries/ParameterSummary.tsx rename to src/core/components/atoms/summaries/ParameterSummary.tsx diff --git a/src/components/containers/BreadCrumbs.tsx b/src/core/components/containers/BreadCrumbs.tsx similarity index 100% rename from src/components/containers/BreadCrumbs.tsx rename to src/core/components/containers/BreadCrumbs.tsx diff --git a/src/components/containers/CollapsibleList.tsx b/src/core/components/containers/CollapsibleList.tsx similarity index 100% rename from src/components/containers/CollapsibleList.tsx rename to src/core/components/containers/CollapsibleList.tsx diff --git a/src/components/containers/ConfirmationModal.tsx b/src/core/components/containers/ConfirmationModal.tsx similarity index 98% rename from src/components/containers/ConfirmationModal.tsx rename to src/core/components/containers/ConfirmationModal.tsx index 17ab25e..cbcd7e7 100644 --- a/src/components/containers/ConfirmationModal.tsx +++ b/src/core/components/containers/ConfirmationModal.tsx @@ -33,7 +33,7 @@ const confirmDialogue: ConfirmationDialogueTemplates = { }, delete: { header: `Delete ${placeholder} ${placeholder}?`, - body: `When you delete the ${placeholder} named ${placeholder}, it will be removed from each component that uses it. + body: `When you delete the ${placeholder} named ${placeholder}, it will be removed from each component that uses it. This definition has %s dependent components.`, button: 'Delete', buttonVariant: 'dangerous', diff --git a/src/components/containers/DefinitionsContainer.tsx b/src/core/components/containers/DefinitionsContainer.tsx similarity index 98% rename from src/components/containers/DefinitionsContainer.tsx rename to src/core/components/containers/DefinitionsContainer.tsx index 8aaf730..cc6cb8f 100644 --- a/src/components/containers/DefinitionsContainer.tsx +++ b/src/core/components/containers/DefinitionsContainer.tsx @@ -1,5 +1,5 @@ import { useRef } from 'react'; -import InspectableMapping from '../../mappings/InspectableMapping'; +import InspectableMapping from '../../../mappings/InspectableMapping'; import { mapDefinitions, NamedGenerable } from '../../state/DefinitionStore'; import { useStoreActions, useStoreState } from '../../state/Hooks'; import AddButton from '../atoms/AddButton'; diff --git a/src/components/containers/DropdownContainer.tsx b/src/core/components/containers/DropdownContainer.tsx similarity index 100% rename from src/components/containers/DropdownContainer.tsx rename to src/core/components/containers/DropdownContainer.tsx diff --git a/src/components/containers/ExternalLinks.tsx b/src/core/components/containers/ExternalLinks.tsx similarity index 93% rename from src/components/containers/ExternalLinks.tsx rename to src/core/components/containers/ExternalLinks.tsx index 1cdea3a..18c01b3 100644 --- a/src/components/containers/ExternalLinks.tsx +++ b/src/core/components/containers/ExternalLinks.tsx @@ -1,5 +1,5 @@ import MoreIcon from '../../icons/ui/MoreIcon'; -import DropdownContainer from '../containers/DropdownContainer'; +import DropdownContainer from './DropdownContainer'; export const ExternalLinks = () => { const links = [ diff --git a/src/components/containers/FilterPreviewContainer.tsx b/src/core/components/containers/FilterPreviewContainer.tsx similarity index 100% rename from src/components/containers/FilterPreviewContainer.tsx rename to src/core/components/containers/FilterPreviewContainer.tsx diff --git a/src/components/containers/GuideContainer.tsx b/src/core/components/containers/GuideContainer.tsx similarity index 100% rename from src/components/containers/GuideContainer.tsx rename to src/core/components/containers/GuideContainer.tsx diff --git a/src/components/containers/HeaderMenu.tsx b/src/core/components/containers/HeaderMenu.tsx similarity index 83% rename from src/components/containers/HeaderMenu.tsx rename to src/core/components/containers/HeaderMenu.tsx index ff9bc90..efdee59 100644 --- a/src/components/containers/HeaderMenu.tsx +++ b/src/core/components/containers/HeaderMenu.tsx @@ -1,6 +1,6 @@ +import { FlowTools } from '../../../flow/components/FlowTools'; import { Button } from '../atoms/Button'; import { ExternalLinks } from './ExternalLinks'; -import { FlowTools } from '../flow/FlowTools'; import PreviewToolbox from './PreviewToolbox'; export default function HeaderMenu() { diff --git a/src/components/containers/KBarList.tsx b/src/core/components/containers/KBarList.tsx similarity index 100% rename from src/components/containers/KBarList.tsx rename to src/core/components/containers/KBarList.tsx diff --git a/src/components/containers/OrbImportsContainer.tsx b/src/core/components/containers/OrbImportsContainer.tsx similarity index 99% rename from src/components/containers/OrbImportsContainer.tsx rename to src/core/components/containers/OrbImportsContainer.tsx index ef04f34..d9b0173 100644 --- a/src/components/containers/OrbImportsContainer.tsx +++ b/src/core/components/containers/OrbImportsContainer.tsx @@ -21,11 +21,11 @@ const OrbImportsContainer = (props: OrbImportProps) => { const ref = useRef(null); const orbDefinitions = useMemo( () => - + mapDefinitions(items, (orb) => { return ( ) }) diff --git a/src/components/flow/GhostNode.tsx b/src/flow/components/GhostNode.tsx similarity index 94% rename from src/components/flow/GhostNode.tsx rename to src/flow/components/GhostNode.tsx index d83b8a6..eb201ca 100644 --- a/src/components/flow/GhostNode.tsx +++ b/src/flow/components/GhostNode.tsx @@ -1,7 +1,7 @@ import { useEffect, useMemo } from 'react'; import { Handle, NodeProps, Position, ReactFlowState, useStore } from 'reactflow'; -import { FlowMode } from '../../state/FlowStore'; -import { useStoreState } from '../../state/Hooks'; +import { useStoreState } from '../../core/state/Hooks'; +import { FlowMode } from '../state/FlowStore'; const connectionNodeIdSelector = (state: ReactFlowState) => state.connectionNodeId; diff --git a/src/components/flow/JobNode.tsx b/src/flow/components/JobNode.tsx similarity index 76% rename from src/components/flow/JobNode.tsx rename to src/flow/components/JobNode.tsx index e0ed004..6d46276 100644 --- a/src/components/flow/JobNode.tsx +++ b/src/flow/components/JobNode.tsx @@ -1,24 +1,26 @@ + import { useEffect, useMemo } from 'react'; +import {workflow} from '@circleci/circleci-config-sdk'; import { Handle, NodeProps, Position, ReactFlowState, useStore } from 'reactflow'; -import JobIcon from '../../icons/components/JobIcon'; -import { FlowMode } from '../../state/FlowStore'; -import { useStoreState } from '../../state/Hooks'; +import JobIcon from '../../core/icons/components/JobIcon'; +import { useStoreState } from '../../core/state/Hooks'; +import { FlowMode } from '../state/FlowStore'; const connectionNodeIdSelector = (state: ReactFlowState) => state.connectionNodeId; -export default function JobNode({ id, selected }: NodeProps) { +export default function JobNode({ id, selected, data }: NodeProps) { const connectionNodeId = useStore(connectionNodeIdSelector); const mode = useStoreState((state) => state.mode); - + const workflowJob = data as workflow.WorkflowJob; const isTarget = connectionNodeId && connectionNodeId !== id; - const label = 'Job Name'; + const label = useMemo(() => workflowJob.parameters?.name || workflowJob.name, [data]); const nodeBase = "p-2 pr-3 text-sm bg-white node flex flex-row text-black rounded-md border cursor-pointer" const outline = selected ? "border-circle-blue" : " border-circle-gray-300" const Handles = useMemo(() => { const connectMode = mode == FlowMode.CONNECT; - const handleBase = `${ connectMode ? '!w-full !h-full' : '!w-0 h-0'} !rounded opacity-50` + const handleBase = `${ connectMode ? '!w-full !h-full' : '!w-0 h-0'} !rounded opacity-0` const notConnecting = connectionNodeId == null return ( diff --git a/src/flow/hooks/useFlowDnD.ts b/src/flow/hooks/useFlowDnD.ts new file mode 100644 index 0000000..0aa12ed --- /dev/null +++ b/src/flow/hooks/useFlowDnD.ts @@ -0,0 +1,71 @@ +import { useCallback, useState } from "react"; +import { applyNodeChanges, useReactFlow, XYPosition } from "reactflow"; +import { v4 } from "uuid"; +import { useStoreActions, useStoreState } from "../../core/state/Hooks"; +import InspectableMapping from "../../mappings/InspectableMapping"; +import { NodeMapping } from "../../mappings/NodeMapping"; +import { FlowActionsModel, FlowStoreModel } from "../state/FlowStore"; +import { useFlowOffset } from "./useFlowOffset"; + +// Alternative to control a node during drag and drop cycle +// If abstracted a layer higher, useDragAndDrop may come in handy later +export function useFlowDragAndDrop(offset?: DOMRect) { + const { addNodes, getNodes } = useReactFlow(); + const dragging = useStoreState((state) => state.dragging); + const cancel = useCallback((e: React.DragEvent) => { e.preventDefault() }, []); + const onDrop = useFlowOffset((position, droppedItem) => { + const mapping = dragging?.dataMapping as unknown as NodeMapping & InspectableMapping; + + if (dragging == undefined || !mapping?.node) { + return; + } + + const nodes = getNodes(); + const data = mapping.node.transform(dragging.data, nodes); + const id = mapping.node.getId(data) + + addNodes({  id, position, type: mapping.node.key, data}); + }, offset, [dragging]); + + return { onDragEnter: cancel, onDragOver: cancel, onDrop } +} + +// Alternative to control a node during drag and drop cycle +// export function useFlowDragAndDrop(offset?: DOMRect) { +// const { addNodes, setNodes, deleteElements, getNodes } = useReactFlow(); +// const [lastPos, setPos] = useState(); + +// // Upon entering the flow, create the drop ghost +// const onDragEnter = useFlowOffset((position, data) => { +// addNodes({  id: 'ghost', position, type: 'ghost',data: ''}); +// }, offset); + +// // While dragging, move the ghost +// const onDragOver = useFlowOffset((position, data) => { +// if (lastPos) { +// const dx = lastPos.x - position.x; +// const dy = lastPos.y - position.y; +// const dist = Math.abs(dx * dy); + +// if (dist > 150) { +// console.log(dist) +// const nodes = getNodes(); +// const changes = applyNodeChanges([ {  id: 'ghost', type: 'position', position, dragging: true } ], nodes); +// setNodes(changes); +// setPos(position); +// } +// } else { +// setPos(position); +// } +// }, offset, [lastPos]); +// // Upon drop and leave, remove the ghost` +// const onDragLeave = useFlowOffset((pos, data) => { +// deleteElements({ nodes: [{ id: 'ghost' }]}) +// }, offset); +// const onDrop = useFlowOffset((position, data) => { +// addNodes({  id: 'ghost', position, type: 'job', data}); +// }, offset); + + +// return [onDragEnter, onDragOver, onDragLeave, onDrop] +// } diff --git a/src/flow/hooks/useFlowOffset.ts b/src/flow/hooks/useFlowOffset.ts new file mode 100644 index 0000000..5cc8250 --- /dev/null +++ b/src/flow/hooks/useFlowOffset.ts @@ -0,0 +1,18 @@ +import { useCallback } from "react"; +import { useReactFlow, XYPosition } from "reactflow"; +import { useStoreState } from "../../core/state/Hooks"; +import { DataModel } from "../../core/state/Store"; + +type FlowEvent = (position: XYPosition, dragging?: DataModel) => void; + +export function useFlowOffset(func: FlowEvent, offset?: DOMRect, deps?: any[]) { + const { project } = useReactFlow(); + + return useCallback((e: React.MouseEvent) => { + const eventPos = project({ x: e.clientX, y: e.clientY - (offset?.y || 0)}); + + func(eventPos); + + e.preventDefault(); + }, deps ? [project, ...deps] : [project]); +} diff --git a/src/state/FlowStore.tsx b/src/flow/state/FlowStore.tsx similarity index 78% rename from src/state/FlowStore.tsx rename to src/flow/state/FlowStore.tsx index b2a942f..342ecb7 100644 --- a/src/state/FlowStore.tsx +++ b/src/flow/state/FlowStore.tsx @@ -1,5 +1,5 @@ import { Action, action } from "easy-peasy"; -import { DataModel } from "./Store"; +import { DataModel } from "../../core/state/Store"; export enum FlowMode { SELECT, @@ -17,12 +17,16 @@ export type FlowStoreModel = { export type FlowActionsModel = { setMode: Action; + setDragging: Action; } export const FlowActions: FlowActionsModel = { setMode: action((state, mode) => { state.mode = mode; - }) + }), + setDragging: action((state, payload) => { + state.dragging = payload; + }), } /** diff --git a/src/util/FlowUtil.tsx b/src/flow/util/FlowUtil.tsx similarity index 100% rename from src/util/FlowUtil.tsx rename to src/flow/util/FlowUtil.tsx diff --git a/src/hooks/useMapping.tsx b/src/hooks/useMapping.tsx deleted file mode 100644 index 821180c..0000000 --- a/src/hooks/useMapping.tsx +++ /dev/null @@ -1,5 +0,0 @@ -export type MappingOpts = { } - -export const useMapping = () => { - -} \ No newline at end of file diff --git a/src/mappings/GenerableMapping.tsx b/src/mappings/GenerableMapping.tsx index 23ead9e..2746462 100644 --- a/src/mappings/GenerableMapping.tsx +++ b/src/mappings/GenerableMapping.tsx @@ -11,8 +11,8 @@ import { DefinitionType, MappingSubscriptions, NamedGenerable, -} from '../state/DefinitionStore'; -import Store, { StoreActionsModel, StoreModel, UpdateType } from '../state/Store'; +} from '../core/state/DefinitionStore'; +import Store, { StoreActionsModel, StoreModel, UpdateType } from '../core/state/Store'; import { CommandMapping } from './components/CommandMapping'; import { ExecutorMapping } from './components/ExecutorMapping'; import { JobMapping } from './components/JobMapping'; diff --git a/src/mappings/InspectableMapping.tsx b/src/mappings/InspectableMapping.tsx index 4dbe452..862932f 100644 --- a/src/mappings/InspectableMapping.tsx +++ b/src/mappings/InspectableMapping.tsx @@ -1,13 +1,13 @@ import { Generable } from '@circleci/circleci-config-sdk/dist/src/lib/Components'; import { FormikValues } from 'formik'; import { Edge, NodeProps } from 'reactflow'; -import { IconProps } from '../icons/IconProps'; +import { IconProps } from '../core/icons/IconProps'; import { DefinitionsModel, DefinitionSubscriptions, NamedGenerable, -} from '../state/DefinitionStore'; -import { NavigationComponent } from '../state/Store'; +} from '../core/state/DefinitionStore'; +import { NavigationComponent } from '../core/state/Store'; import { WorkflowStage } from './components/WorkflowMapping'; import GenerableMapping, { SubTypeMapping } from './GenerableMapping'; export interface GenerableInfoType { @@ -61,16 +61,6 @@ export default interface InspectableMapping< data: GenerableType, nodeData: ConfigNodeProps, ) => { [K in KeysOfUnion]?: any }; - node?: { - /** Transform definition data */ - transform?: ( - data: GenerableType, - extras?: any, - stage?: { nodes: Node[], edges: Edge[] }, - ) => ConfigNodeProps; - /** @todo: Add store functionality to better support updating definitions and their corresponding workflow nodes */ - component: React.FunctionComponent<{ data: ConfigNodeProps } & NodeProps>; - }; subtypes?: { component: NavigationComponent; getSubtype: (data: GenerableType) => string | undefined; diff --git a/src/mappings/NodeMapping.tsx b/src/mappings/NodeMapping.tsx new file mode 100644 index 0000000..8272837 --- /dev/null +++ b/src/mappings/NodeMapping.tsx @@ -0,0 +1,22 @@ +import { Node, NodeProps } from "reactflow"; +import { NamedGenerable } from "../core/state/DefinitionStore"; + +export interface NodeMapping +{ + node: { + /** + * generated id from managed type + */ + getId: (data: ManagedType) => string; + key: string; + /** Transform definition data */ + transform: ( + data: InType, + nodes: Node[], + extras?: any, + ) => ManagedType; + /** @todo: Add store functionality to better support updating definitions and their corresponding workflow nodes */ + component: React.FunctionComponent<{ data: ManagedType } & NodeProps>; + }; +} diff --git a/src/mappings/components/CommandMapping.tsx b/src/mappings/components/CommandMapping.tsx index ffceda4..5133513 100644 --- a/src/mappings/components/CommandMapping.tsx +++ b/src/mappings/components/CommandMapping.tsx @@ -1,13 +1,13 @@ import { parseReusableCommand } from '@circleci/circleci-config-parser'; import { reusable } from '@circleci/circleci-config-sdk'; -import CommandSummary from '../../components/atoms/summaries/CommandSummary'; -import CommandInspector from '../../components/containers/inspector/CommandInspector'; -import { componentParametersSubtypes } from '../../components/containers/inspector/subtypes/ParameterSubtypes'; -import CommandIcon from '../../icons/components/CommandIcon'; +import CommandSummary from '../../core/components/atoms/summaries/CommandSummary'; +import CommandInspector from '../../core/components/containers/inspector/CommandInspector'; +import { componentParametersSubtypes } from '../../core/components/containers/inspector/subtypes/ParameterSubtypes'; +import CommandIcon from '../../core/icons/components/CommandIcon'; import { DefinitionAction, definitionsAsArray, -} from '../../state/DefinitionStore'; +} from '../../core/state/DefinitionStore'; import InspectableMapping from '../InspectableMapping'; export const CommandMapping: InspectableMapping = { diff --git a/src/mappings/components/ExecutorMapping.tsx b/src/mappings/components/ExecutorMapping.tsx index 23f8677..808f62b 100644 --- a/src/mappings/components/ExecutorMapping.tsx +++ b/src/mappings/components/ExecutorMapping.tsx @@ -5,13 +5,13 @@ import { workflow, } from '@circleci/circleci-config-sdk'; import { parseReusableExecutor } from '@circleci/circleci-config-parser'; -import ExecutorSummary from '../../components/atoms/summaries/ExecutorSummary'; -import ExecutorInspector from '../../components/containers/inspector/ExecutorInspector'; -import { executorSubtypes } from '../../components/containers/inspector/subtypes/ExecutorSubtypes'; -import { componentParametersSubtypes } from '../../components/containers/inspector/subtypes/ParameterSubtypes'; -import ExecutorTypePageNav from '../../components/menus/definitions/subtypes/ExecutorTypePage'; -import ExecutorIcon from '../../icons/components/ExecutorIcon'; -import { DefinitionAction } from '../../state/DefinitionStore'; +import ExecutorSummary from '../../core/components/atoms/summaries/ExecutorSummary'; +import ExecutorInspector from '../../core/components/containers/inspector/ExecutorInspector'; +import { executorSubtypes } from '../../core/components/containers/inspector/subtypes/ExecutorSubtypes'; +import { componentParametersSubtypes } from '../../core/components/containers/inspector/subtypes/ParameterSubtypes'; +import ExecutorTypePageNav from '../../core/components/menus/definitions/subtypes/ExecutorTypePage'; +import ExecutorIcon from '../../core/icons/components/ExecutorIcon'; +import { DefinitionAction } from '../../core/state/DefinitionStore'; import InspectableMapping from '../InspectableMapping'; import { JobMapping } from './JobMapping'; diff --git a/src/mappings/components/JobMapping.tsx b/src/mappings/components/JobMapping.tsx index da70577..9fed511 100644 --- a/src/mappings/components/JobMapping.tsx +++ b/src/mappings/components/JobMapping.tsx @@ -1,18 +1,19 @@ import { parseJob } from '@circleci/circleci-config-parser'; import { Job, orb, reusable, workflow } from '@circleci/circleci-config-sdk'; -import JobNode from '../../components/atoms/nodes/JobNode'; -import JobSummary from '../../components/atoms/summaries/JobSummary'; -import JobInspector from '../../components/containers/inspector/JobInspector'; -import { componentParametersSubtypes } from '../../components/containers/inspector/subtypes/ParameterSubtypes'; -import JobIcon from '../../icons/components/JobIcon'; +import JobSummary from '../../core/components/atoms/summaries/JobSummary'; +import JobInspector from '../../core/components/containers/inspector/JobInspector'; +import { componentParametersSubtypes } from '../../core/components/containers/inspector/subtypes/ParameterSubtypes'; +import JobIcon from '../../core/icons/components/JobIcon'; import { DefinitionAction, definitionsAsArray, -} from '../../state/DefinitionStore'; +} from '../../core/state/DefinitionStore'; +import JobNode from '../../flow/components/JobNode'; import InspectableMapping from '../InspectableMapping'; +import { NodeMapping } from '../NodeMapping'; import { UNDEFINED_EXECUTOR } from './ExecutorMapping'; -export const JobMapping: InspectableMapping = { +export const JobMapping: InspectableMapping & NodeMapping = { key: 'jobs', name: { singular: 'Job', @@ -101,36 +102,40 @@ export const JobMapping: InspectableMapping = { remove: (actions) => actions.delete_jobs, }, dragTarget: 'workflow', - // node: { - // transform: (data, params, elements) => { - // const stagedNames = new Set( - // elements - // ?.filter((element) => element.type === 'jobs') - // .map((job) => job.data.parameters?.name || job.data.name), - // ); - // let name = data.name; + node: { + getId: (data) => { + return data.parameters?.name || data.name; + }, + transform: (data, nodes, params) => { + const stagedNames = new Set( + nodes + ?.filter((element) => element.type === 'workflow_job') + .map((node) => node.data.parameters?.name || node.data.name), + ); + let name = data.name; - // if (stagedNames && stagedNames.has(name)) { - // for (let i = 2; true; i++) { - // const newName = `${name} (${i})`; + if (stagedNames.has(name)) { + for (let i = 2; true; i++) { + const newName = `${name} (${i})`; - // if (!stagedNames.has(newName)) { - // name = newName; - // break; - // } - // } - // } + if (!stagedNames.has(newName)) { + name = newName; + break; + } + } + } - // const newParams = params || {}; + const newParams = params || {}; - // if (name !== data.name) { - // newParams.name = name; - // } + if (name !== data.name) { + newParams.name = name; + } - // return new workflow.WorkflowJob(data, newParams); - // }, - // component: JobNode, - // }, + return new workflow.WorkflowJob(data, newParams); + }, + component: JobNode, + key: 'workflow_job' + }, components: { icon: JobIcon, summary: JobSummary, diff --git a/src/mappings/components/ParameterMapping.tsx b/src/mappings/components/ParameterMapping.tsx index aff6e55..aafa2a7 100644 --- a/src/mappings/components/ParameterMapping.tsx +++ b/src/mappings/components/ParameterMapping.tsx @@ -2,12 +2,12 @@ import { parseParameter } from '@circleci/circleci-config-parser'; import { parameters } from '@circleci/circleci-config-sdk'; import { CustomParameter } from '@circleci/circleci-config-sdk/dist/src/lib/Components/Parameters'; import { PipelineParameterLiteral } from '@circleci/circleci-config-sdk/dist/src/lib/Components/Parameters/types/CustomParameterLiterals.types'; -import ParameterSummary from '../../components/atoms/summaries/ParameterSummary'; -import ParameterInspector from '../../components/containers/inspector/ParameterInspector'; -import { parameterSubtypes } from '../../components/containers/inspector/subtypes/ParameterSubtypes'; -import ParameterTypePageNav from '../../components/menus/definitions/subtypes/ParameterTypePage'; -import ParameterIcon from '../../icons/components/ParameterIcon'; -import { DefinitionAction } from '../../state/DefinitionStore'; +import ParameterSummary from '../../core/components/atoms/summaries/ParameterSummary'; +import ParameterInspector from '../../core/components/containers/inspector/ParameterInspector'; +import { parameterSubtypes } from '../../core/components/containers/inspector/subtypes/ParameterSubtypes'; +import ParameterTypePageNav from '../../core/components/menus/definitions/subtypes/ParameterTypePage'; +import ParameterIcon from '../../core/icons/components/ParameterIcon'; +import { DefinitionAction } from '../../core/state/DefinitionStore'; import InspectableMapping from '../InspectableMapping'; export const ParameterMapping: InspectableMapping< diff --git a/src/mappings/components/WorkflowMapping.tsx b/src/mappings/components/WorkflowMapping.tsx index a434244..922518f 100644 --- a/src/mappings/components/WorkflowMapping.tsx +++ b/src/mappings/components/WorkflowMapping.tsx @@ -2,8 +2,8 @@ import { Job, workflow, Workflow } from '@circleci/circleci-config-sdk'; import { When } from '@circleci/circleci-config-sdk/dist/src/lib/Components/Logic'; import { WorkflowJobAbstract } from '@circleci/circleci-config-sdk/dist/src/lib/Components/Workflow'; import { Edge, Node } from 'reactflow'; -import { Definition, DefinitionAction } from '../../state/DefinitionStore'; -import { StoreModel } from '../../state/Store'; +import { Definition, DefinitionAction } from '../../core/state/DefinitionStore'; +import { StoreModel } from '../../core/state/Store'; import GenerableMapping from '../GenerableMapping'; export type WorkflowElements = { nodes: Node[], edges: Edge[]};