Workflow Editor UI for n8n - a comprehensive Vue.js-based visual workflow editor with drag-and-drop functionality.
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
Interactive canvas functionality for visual workflow editing with drag-and-drop node placement, connection management, and canvas navigation.
Main canvas component providing the visual workflow editor interface.
/**
* Canvas component props for workflow editor
*/
interface CanvasProps {
id?: string;
nodes: CanvasNode[];
connections: CanvasConnection[];
controlsPosition?: PanelPosition;
eventBus?: EventBus<CanvasEventBusEvents>;
readOnly?: boolean;
executing?: boolean;
keyBindings?: boolean;
loading?: boolean;
}
/**
* Canvas component events
*/
interface CanvasEvents {
'update:modelValue': (elements: CanvasElement[]) => void;
'update:node:position': (id: string, position: XYPosition) => void;
'click:node': (id: string, event: MouseEvent) => void;
'create:node': (nodeType: string, position: XYPosition) => void;
'delete:node': (id: string) => void;
'run:workflow': () => void;
'save:workflow': () => void;
}
type PanelPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';Data structure representing nodes on the canvas.
interface CanvasNodeData {
id: string;
name: string;
subtitle: string;
type: string;
typeVersion: number;
disabled: boolean;
inputs: CanvasConnectionPort[];
outputs: CanvasConnectionPort[];
connections: {
[CanvasConnectionMode.Input]: INodeConnections;
[CanvasConnectionMode.Output]: INodeConnections;
};
issues: {
items: string[];
visible: boolean;
};
pinnedData: {
count: number;
visible: boolean;
};
execution: {
status?: ExecutionStatus;
waiting?: string;
running: boolean;
};
runData: {
outputMap?: ExecutionOutputMap;
iterations: number;
visible: boolean;
};
render: CanvasNodeRender;
}
interface CanvasConnectionPort {
node?: string;
type: NodeConnectionType;
index: number;
required?: boolean;
maxConnections?: number;
label?: string;
}
interface CanvasNodeRender {
type: CanvasNodeRenderType;
options: CanvasNodeRenderOptions;
}
enum CanvasConnectionMode {
Input = 'inputs',
Output = 'outputs'
}
type NodeConnectionType = 'main' | 'ai_memory' | 'ai_document' | 'ai_tool' | 'ai_languageModel' | 'ai_embedding' | 'ai_vectorStore' | 'ai_textSplitter' | 'ai_outputParser';Composable providing canvas manipulation functions.
/**
* Composable for canvas operations
*/
function useCanvasOperations(): {
addNodes(nodes: AddedNode[], connections?: AddedNodeConnection[]): void;
deleteNode(id: string): void;
copyNodes(ids: string[]): void;
cutNodes(ids: string[]): void;
pasteNodes(position?: XYPosition): void;
selectAllNodes(): void;
deselectAllNodes(): void;
revertDeleteNode(id: string): void;
setNodeSelected(id: string, selected?: boolean): void;
updateNodePosition(id: string, position: XYPosition): void;
createConnection(source: CanvasConnectionPort, target: CanvasConnectionPort): void;
deleteConnection(connection: CanvasConnection): void;
};
interface AddedNode {
type: string;
openDetail?: boolean;
isAutoAdd?: boolean;
actionName?: string;
position?: XYPosition;
name?: string;
}
interface AddedNodeConnection {
from: {
nodeIndex: number;
outputIndex?: number;
type?: NodeConnectionType;
};
to: {
nodeIndex: number;
inputIndex?: number;
type?: NodeConnectionType;
};
}Usage Example:
import { useCanvasOperations } from '@/composables/useCanvasOperations';
const canvasOperations = useCanvasOperations();
// Add multiple nodes with connections
canvasOperations.addNodes([
{
type: 'n8n-nodes-base.httpRequest',
position: [100, 100],
name: 'Fetch Data'
},
{
type: 'n8n-nodes-base.set',
position: [300, 100],
name: 'Process Data'
}
], [
{
from: { nodeIndex: 0, outputIndex: 0 },
to: { nodeIndex: 1, inputIndex: 0 }
}
]);
// Select and delete nodes
canvasOperations.setNodeSelected('node-1', true);
canvasOperations.deleteNode('node-1');Automatic node layout and arrangement functionality.
/**
* Composable for canvas layout operations
*/
function useCanvasLayout(): {
layout(target: 'all' | 'selection'): void;
getLayoutedElements(nodes: CanvasNode[], connections: CanvasConnection[]): {
nodes: CanvasNode[];
connections: CanvasConnection[];
};
arrangeNodes(nodes: INodeUi[], direction?: 'horizontal' | 'vertical'): INodeUi[];
};
/**
* Automatically arrange nodes on canvas
* @param target - Whether to layout all nodes or just selected ones
*/
function layout(target: 'all' | 'selection'): void;
/**
* Calculate optimal positions for nodes and connections
* @param nodes - Canvas nodes to layout
* @param connections - Canvas connections
* @returns Layouted elements with new positions
*/
function getLayoutedElements(
nodes: CanvasNode[],
connections: CanvasConnection[]
): {
nodes: CanvasNode[];
connections: CanvasConnection[];
};Composable for accessing canvas node data within node components.
/**
* Composable for accessing canvas node data
*/
function useCanvasNode(): UseCanvasNodeReturn;
interface UseCanvasNodeReturn {
node: InjectedCanvasNode;
id: ComputedRef<string>;
name: ComputedRef<string>;
label: ComputedRef<string>;
subtitle: ComputedRef<string>;
inputs: ComputedRef<CanvasConnectionPort[]>;
outputs: ComputedRef<CanvasConnectionPort[]>;
isDisabled: ComputedRef<boolean>;
isReadOnly: ComputedRef<boolean>;
isSelected: ComputedRef<boolean>;
hasPinnedData: ComputedRef<boolean>;
hasIssues: ComputedRef<boolean>;
executionRunning: ComputedRef<boolean>;
executionWaiting: ComputedRef<boolean>;
executionStatus: ComputedRef<ExecutionStatus | undefined>;
}
interface InjectedCanvasNode {
id: string;
label: string;
data: CanvasNodeData;
}Utility functions for canvas positioning and calculations.
/**
* Calculate new node position avoiding conflicts
* @param nodes - Existing nodes
* @param position - Initial position
* @param options - Position calculation options
* @returns Conflict-free position
*/
function getNewNodePosition(
nodes: INodeUi[],
position: XYPosition,
options?: {
offsetX?: number;
offsetY?: number;
gridSize?: number;
}
): XYPosition;
/**
* Snap position to grid alignment
* @param position - Position to snap
* @param gridSize - Grid size for snapping
* @returns Grid-aligned position
*/
function snapPositionToGrid(position: XYPosition, gridSize?: number): XYPosition;
/**
* Get center position of canvas viewport
* @param scale - Current canvas scale
* @param offset - Current canvas offset
* @returns Center position coordinates
*/
function getMidCanvasPosition(scale: number, offset: XYPosition): XYPosition;
/**
* Calculate node dimensions based on configuration
* @param isConfiguration - Whether node is in configuration mode
* @param isConfigurable - Whether node is configurable
* @param inputCount - Number of input ports
* @param outputCount - Number of output ports
* @param nonMainInputCount - Number of non-main inputs
* @returns Node dimensions [width, height]
*/
function calculateNodeSize(
isConfiguration: boolean,
isConfigurable: boolean,
inputCount: number,
outputCount: number,
nonMainInputCount: number
): [number, number];
// Canvas constants
const GRID_SIZE = 16;
const DEFAULT_NODE_SIZE: [number, number] = [96, 96];
const CONFIGURATION_NODE_SIZE: [number, number] = [80, 80];
const DEFAULT_START_POSITION_X = 176;
const DEFAULT_START_POSITION_Y = 240;Event bus system for canvas communication.
interface CanvasEventBusEvents {
fitView: never;
'nodes:select': { ids: string[] };
'nodes:action': { ids: string[]; action: string };
'nodes:delete': { ids: string[] };
'saved:workflow': never;
'open:contextmenu': { event: MouseEvent; type: 'canvas' | 'node'; nodeId?: string };
}
/**
* Canvas event bus for component communication
*/
interface EventBus<T> {
emit<K extends keyof T>(event: K, ...args: T[K] extends never ? [] : [T[K]]): void;
on<K extends keyof T>(event: K, handler: T[K] extends never ? () => void : (arg: T[K]) => void): void;
off<K extends keyof T>(event: K, handler: Function): void;
}Canvas viewport navigation and zoom controls.
/**
* Canvas navigation and zoom functionality
*/
interface CanvasNavigation {
fitView(): void;
zoomIn(): void;
zoomOut(): void;
zoomToFit(): void;
resetView(): void;
panTo(position: XYPosition): void;
setViewport(viewport: CanvasViewport): void;
}
interface CanvasViewport {
x: number;
y: number;
zoom: number;
}
interface IZoomConfig {
scale: number;
offset: XYPosition;
origin?: XYPosition;
}type XYPosition = [number, number];
interface CanvasElement {
id: string;
type: 'node' | 'connection';
position?: XYPosition;
data?: any;
}
interface CanvasConnection {
id: string;
source: string;
target: string;
sourceHandle?: string;
targetHandle?: string;
type?: string;
data?: any;
}
interface EndpointStyle {
width?: number;
height?: number;
fill?: string;
stroke?: string;
outlineStroke?: string;
lineWidth?: number;
hover?: boolean;
showOutputLabel?: boolean;
size?: string;
hoverMessage?: string;
}
interface IBounds {
minX: number;
minY: number;
maxX: number;
maxY: number;
}
type DraggableMode = 'mapping' | 'panel-resize' | 'move';
enum CanvasNodeRenderType {
Default = 'default',
StickyNote = 'sticky-note',
Configuration = 'configuration'
}
interface CanvasNodeRenderOptions {
configurable?: boolean;
configuration?: boolean;
trigger?: boolean;
}