mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-10 08:05:22 +02:00
Add option to edit workflow name
This commit is contained in:
parent
0d9dc1fcf8
commit
add12ab53c
7 changed files with 139 additions and 235 deletions
|
|
@ -21,11 +21,11 @@ import CustomEdge from "../../../components/flow/edges/CustomEdge";
|
|||
import { AgentNode, EndCall, GlobalNode, StartCall } from "../../../components/flow/nodes";
|
||||
import { ConfigurationsDialog } from './components/ConfigurationsDialog';
|
||||
import { TemplateContextVariablesDialog } from './components/TemplateContextVariablesDialog';
|
||||
import { layoutNodes } from './components/WorkflowControls';
|
||||
import WorkflowHeader from "./components/WorkflowHeader";
|
||||
import { WorkflowTabs } from './components/WorkflowTabs';
|
||||
import { WorkflowProvider } from "./contexts/WorkflowContext";
|
||||
import { useWorkflowState } from "./hooks/useWorkflowState";
|
||||
import { layoutNodes } from './utils/layoutNodes';
|
||||
|
||||
// Define the node types dynamically based on the onSave prop
|
||||
const nodeTypes = {
|
||||
|
|
@ -307,6 +307,7 @@ function RenderWorkflow({ initialWorkflowName, workflowId, initialFlow, initialT
|
|||
open={isConfigurationsDialogOpen}
|
||||
onOpenChange={setIsConfigurationsDialogOpen}
|
||||
workflowConfigurations={workflowConfigurations}
|
||||
workflowName={workflowName}
|
||||
onSave={saveWorkflowConfigurations}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
|
|
@ -11,7 +11,8 @@ interface ConfigurationsDialogProps {
|
|||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
workflowConfigurations: WorkflowConfigurations | null;
|
||||
onSave: (configurations: WorkflowConfigurations) => Promise<void>;
|
||||
workflowName: string;
|
||||
onSave: (configurations: WorkflowConfigurations, workflowName: string) => Promise<void>;
|
||||
}
|
||||
|
||||
const DEFAULT_VAD_CONFIG: VADConfiguration = {
|
||||
|
|
@ -30,8 +31,10 @@ export const ConfigurationsDialog = ({
|
|||
open,
|
||||
onOpenChange,
|
||||
workflowConfigurations,
|
||||
workflowName,
|
||||
onSave
|
||||
}: ConfigurationsDialogProps) => {
|
||||
const [name, setName] = useState<string>(workflowName);
|
||||
const [vadConfig, setVadConfig] = useState<VADConfiguration>(
|
||||
workflowConfigurations?.vad_configuration || DEFAULT_VAD_CONFIG
|
||||
);
|
||||
|
|
@ -54,7 +57,7 @@ export const ConfigurationsDialog = ({
|
|||
ambient_noise_configuration: ambientNoiseConfig,
|
||||
max_call_duration: maxCallDuration,
|
||||
max_user_idle_timeout: maxUserIdleTimeout
|
||||
});
|
||||
}, name);
|
||||
onOpenChange(false);
|
||||
} catch (error) {
|
||||
console.error("Failed to save configurations:", error);
|
||||
|
|
@ -63,15 +66,16 @@ export const ConfigurationsDialog = ({
|
|||
}
|
||||
};
|
||||
|
||||
const handleDialogOpenChange = (isOpen: boolean) => {
|
||||
onOpenChange(isOpen);
|
||||
if (isOpen) {
|
||||
// Sync state with props when dialog opens
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setName(workflowName);
|
||||
setVadConfig(workflowConfigurations?.vad_configuration || DEFAULT_VAD_CONFIG);
|
||||
setAmbientNoiseConfig(workflowConfigurations?.ambient_noise_configuration || DEFAULT_AMBIENT_NOISE_CONFIG);
|
||||
setMaxCallDuration(workflowConfigurations?.max_call_duration || 600);
|
||||
setMaxUserIdleTimeout(workflowConfigurations?.max_user_idle_timeout || 10);
|
||||
}
|
||||
};
|
||||
}, [open, workflowName, workflowConfigurations]);
|
||||
|
||||
const handleVadChange = (field: keyof VADConfiguration, value: string) => {
|
||||
const numValue = parseFloat(value);
|
||||
|
|
@ -84,13 +88,35 @@ export const ConfigurationsDialog = ({
|
|||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleDialogOpenChange}>
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Configurations</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Workflow Name Section */}
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold mb-1">Workflow Name</h3>
|
||||
<p className="text-xs text-gray-500">
|
||||
The name of your workflow
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="workflow_name" className="text-xs">
|
||||
Name
|
||||
</Label>
|
||||
<Input
|
||||
id="workflow_name"
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Enter workflow name"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Voice Activity Detection Section */}
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -1,178 +0,0 @@
|
|||
import dagre from '@dagrejs/dagre';
|
||||
import { ReactFlowInstance } from "@xyflow/react";
|
||||
import { Check, Pencil } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
|
||||
import { FlowEdge, FlowNode } from "@/components/flow/types";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { WorkflowConfigurations } from "@/types/workflow-configurations";
|
||||
|
||||
import { ConfigurationsDialog } from "./ConfigurationsDialog";
|
||||
import { TemplateContextVariablesDialog } from "./TemplateContextVariablesDialog";
|
||||
|
||||
interface WorkflowControlsProps {
|
||||
workflowId: number;
|
||||
workflowName: string;
|
||||
isEditingName: boolean;
|
||||
setIsEditingName: (isEditing: boolean) => void;
|
||||
handleNameChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
setIsAddNodePanelOpen: (isOpen: boolean) => void;
|
||||
saveWorkflow: (updateWorkflowDefinition: boolean) => Promise<void>;
|
||||
nodes: FlowNode[];
|
||||
edges: FlowEdge[];
|
||||
setNodes: (nodes: FlowNode[] | ((nds: FlowNode[]) => FlowNode[])) => void;
|
||||
rfInstance: React.RefObject<ReactFlowInstance<FlowNode, FlowEdge> | null>;
|
||||
templateContextVariables?: Record<string, string>;
|
||||
saveTemplateContextVariables: (variables: Record<string, string>) => Promise<void>;
|
||||
workflowConfigurations: WorkflowConfigurations | null;
|
||||
saveWorkflowConfigurations: (configurations: WorkflowConfigurations) => Promise<void>;
|
||||
}
|
||||
|
||||
export const layoutNodes = (
|
||||
nodes: FlowNode[],
|
||||
edges: FlowEdge[],
|
||||
rankdir: 'TB' | 'LR',
|
||||
rfInstance: React.RefObject<ReactFlowInstance<FlowNode, FlowEdge> | null>,
|
||||
saveWorkflow: (updateWorkflowDefinition: boolean) => Promise<void>
|
||||
) => {
|
||||
const g = new dagre.graphlib.Graph();
|
||||
g.setGraph({ rankdir, nodesep: 250, ranksep: 250 });
|
||||
g.setDefaultEdgeLabel(() => ({}));
|
||||
|
||||
// Sort nodes so startCall nodes come first and endCall nodes come last
|
||||
const sortedNodes = [...nodes].sort((a, b) => {
|
||||
if (a.type === 'startCall') return -1;
|
||||
if (b.type === 'startCall') return 1;
|
||||
if (a.type === 'endCall') return 1;
|
||||
if (b.type === 'endCall') return -1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
sortedNodes.forEach((node) => {
|
||||
g.setNode(node.id, { width: 180, height: 60 });
|
||||
});
|
||||
|
||||
edges.forEach((edge) => {
|
||||
g.setEdge(edge.source, edge.target);
|
||||
});
|
||||
|
||||
dagre.layout(g);
|
||||
|
||||
const newNodes = sortedNodes.map((node) => {
|
||||
const nodeWithPosition = g.node(node.id);
|
||||
return {
|
||||
...node,
|
||||
position: { x: nodeWithPosition.x, y: nodeWithPosition.y }
|
||||
};
|
||||
});
|
||||
|
||||
// Fit view to the new layout and save the viewport position
|
||||
setTimeout(() => {
|
||||
rfInstance.current?.fitView();
|
||||
saveWorkflow(true);
|
||||
}, 0);
|
||||
|
||||
return newNodes;
|
||||
};
|
||||
|
||||
const WorkflowControls = ({
|
||||
workflowId,
|
||||
workflowName,
|
||||
isEditingName,
|
||||
setIsEditingName,
|
||||
handleNameChange,
|
||||
setIsAddNodePanelOpen,
|
||||
saveWorkflow,
|
||||
nodes,
|
||||
edges,
|
||||
setNodes,
|
||||
rfInstance,
|
||||
templateContextVariables = {},
|
||||
saveTemplateContextVariables,
|
||||
workflowConfigurations,
|
||||
saveWorkflowConfigurations
|
||||
}: WorkflowControlsProps) => {
|
||||
const router = useRouter();
|
||||
const [isContextVarsDialogOpen, setIsContextVarsDialogOpen] = useState(false);
|
||||
const [isConfigurationsDialogOpen, setIsConfigurationsDialogOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-2">
|
||||
<div className="flex items-center relative bg-white border border-gray-200 rounded-md px-3 py-1 shadow-sm group hover:border-gray-300 transition-colors w-45">
|
||||
{isEditingName ? (
|
||||
<input
|
||||
type="text"
|
||||
value={workflowName}
|
||||
onChange={handleNameChange}
|
||||
className="pr-8 bg-transparent focus:outline-none w-full text-lg"
|
||||
autoFocus
|
||||
onKeyDown={(e) => e.key === 'Enter' && (setIsEditingName(false), saveWorkflow(false))}
|
||||
/>
|
||||
) : (
|
||||
<h1 className="text-lg font-medium pr-8 truncate">{workflowName}</h1>
|
||||
)}
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
if (isEditingName) {
|
||||
setIsEditingName(false);
|
||||
saveWorkflow(false);
|
||||
} else {
|
||||
setIsEditingName(true);
|
||||
}
|
||||
}}
|
||||
className="h-7 w-7 absolute right-2 top-1/2 transform -translate-y-1/2"
|
||||
>
|
||||
{isEditingName ? (
|
||||
<Check className="h-4 w-4 text-green-500" />
|
||||
) : (
|
||||
<Pencil className="h-4 w-4 opacity-50 group-hover:opacity-100 transition-opacity" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Button onClick={() => setIsAddNodePanelOpen(true)}>Add New Node</Button>
|
||||
<Button onClick={() => setNodes(layoutNodes(nodes, edges, 'TB', rfInstance, saveWorkflow))}>Vertical Layout</Button>
|
||||
<Button onClick={() => setNodes(layoutNodes(nodes, edges, 'LR', rfInstance, saveWorkflow))}>Horizontal Layout</Button>
|
||||
<Button
|
||||
onClick={() => setIsConfigurationsDialogOpen(true)}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
Configurations
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setIsContextVarsDialogOpen(true)}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
Template Context Variables
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => router.push(`/workflow/${workflowId}/runs`)}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
View Run History
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ConfigurationsDialog
|
||||
open={isConfigurationsDialogOpen}
|
||||
onOpenChange={setIsConfigurationsDialogOpen}
|
||||
workflowConfigurations={workflowConfigurations}
|
||||
onSave={saveWorkflowConfigurations}
|
||||
/>
|
||||
|
||||
<TemplateContextVariablesDialog
|
||||
open={isContextVarsDialogOpen}
|
||||
onOpenChange={setIsContextVarsDialogOpen}
|
||||
templateContextVariables={templateContextVariables}
|
||||
onSave={saveTemplateContextVariables}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkflowControls;
|
||||
|
|
@ -1,2 +1 @@
|
|||
export * from './WorkflowControls';
|
||||
export * from './WorkflowHeader';
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
import { useRouter } from "next/navigation";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
|
||||
import { useWorkflowStore } from "@/app/workflow/[workflowId]/stores/workflowStore";
|
||||
import {
|
||||
createWorkflowRunApiV1WorkflowWorkflowIdRunsPost,
|
||||
updateWorkflowApiV1WorkflowWorkflowIdPut,
|
||||
|
|
@ -18,7 +19,6 @@ import { WorkflowError } from "@/client/types.gen";
|
|||
import { FlowEdge, FlowNode, NodeType } from "@/components/flow/types";
|
||||
import logger from '@/lib/logger';
|
||||
import { getNextNodeId, getRandomId } from "@/lib/utils";
|
||||
import { useWorkflowStore } from "@/stores/workflowStore";
|
||||
import { WorkflowConfigurations } from "@/types/workflow-configurations";
|
||||
|
||||
export function getDefaultAllowInterrupt(type: string = NodeType.START_CALL): boolean {
|
||||
|
|
@ -102,7 +102,6 @@ export const useWorkflowState = ({
|
|||
workflowName,
|
||||
isDirty,
|
||||
isAddNodePanelOpen,
|
||||
isEditingName,
|
||||
workflowValidationErrors,
|
||||
templateContextVariables,
|
||||
workflowConfigurations,
|
||||
|
|
@ -316,20 +315,18 @@ export const useWorkflowState = ({
|
|||
|
||||
const onEdgesChange: OnEdgesChange = useCallback(
|
||||
(changes) => {
|
||||
setEdges((eds) => {
|
||||
const newEdges = applyEdgeChanges(changes, eds) as FlowEdge[];
|
||||
return newEdges;
|
||||
});
|
||||
const currentEdges = useWorkflowStore.getState().edges;
|
||||
const newEdges = applyEdgeChanges(changes, currentEdges) as FlowEdge[];
|
||||
setEdges(newEdges, changes);
|
||||
},
|
||||
[setEdges],
|
||||
);
|
||||
|
||||
const onNodesChange: OnNodesChange = useCallback(
|
||||
(changes) => {
|
||||
setNodes((nds) => {
|
||||
const newNodes = applyNodeChanges(changes, nds) as FlowNode[];
|
||||
return newNodes;
|
||||
});
|
||||
const currentNodes = useWorkflowStore.getState().nodes;
|
||||
const newNodes = applyNodeChanges(changes, currentNodes) as FlowNode[];
|
||||
setNodes(newNodes, changes);
|
||||
},
|
||||
[setNodes],
|
||||
);
|
||||
|
|
@ -380,7 +377,7 @@ export const useWorkflowState = ({
|
|||
}, [workflowId, workflowName, user, getAccessToken, setTemplateContextVariables]);
|
||||
|
||||
// Save workflow configurations
|
||||
const saveWorkflowConfigurations = useCallback(async (configurations: WorkflowConfigurations) => {
|
||||
const saveWorkflowConfigurations = useCallback(async (configurations: WorkflowConfigurations, newWorkflowName: string) => {
|
||||
if (!user) return;
|
||||
const accessToken = await getAccessToken();
|
||||
try {
|
||||
|
|
@ -389,7 +386,7 @@ export const useWorkflowState = ({
|
|||
workflow_id: workflowId,
|
||||
},
|
||||
body: {
|
||||
name: workflowName,
|
||||
name: newWorkflowName,
|
||||
workflow_definition: null,
|
||||
workflow_configurations: configurations as Record<string, unknown>,
|
||||
},
|
||||
|
|
@ -398,12 +395,13 @@ export const useWorkflowState = ({
|
|||
},
|
||||
});
|
||||
setWorkflowConfigurations(configurations);
|
||||
setWorkflowName(newWorkflowName);
|
||||
logger.info('Workflow configurations saved successfully');
|
||||
} catch (error) {
|
||||
logger.error(`Error saving workflow configurations: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}, [workflowId, workflowName, user, getAccessToken, setWorkflowConfigurations]);
|
||||
}, [workflowId, user, getAccessToken, setWorkflowConfigurations, setWorkflowName]);
|
||||
|
||||
// Update rfInstance when it changes
|
||||
useEffect(() => {
|
||||
|
|
@ -423,7 +421,6 @@ export const useWorkflowState = ({
|
|||
edges,
|
||||
isAddNodePanelOpen,
|
||||
workflowName,
|
||||
isEditingName,
|
||||
isDirty,
|
||||
workflowValidationErrors,
|
||||
templateContextVariables,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { ReactFlowInstance } from '@xyflow/react';
|
||||
import { NodeChange, EdgeChange } from '@xyflow/system';
|
||||
import { create } from 'zustand';
|
||||
|
||||
import { WorkflowError } from '@/client/types.gen';
|
||||
|
|
@ -27,7 +28,6 @@ interface WorkflowState {
|
|||
// UI state (not tracked in history)
|
||||
isDirty: boolean;
|
||||
isAddNodePanelOpen: boolean;
|
||||
isEditingName: boolean;
|
||||
|
||||
// Validation state
|
||||
workflowValidationErrors: WorkflowError[];
|
||||
|
|
@ -59,13 +59,13 @@ interface WorkflowActions {
|
|||
canRedo: () => boolean;
|
||||
|
||||
// Node operations
|
||||
setNodes: (nodes: FlowNode[] | ((nodes: FlowNode[]) => FlowNode[])) => void;
|
||||
setNodes: (nodes: FlowNode[], changes?: NodeChange<FlowNode>[]) => void;
|
||||
addNode: (node: FlowNode) => void;
|
||||
updateNode: (nodeId: string, updates: Partial<FlowNode>) => void;
|
||||
deleteNode: (nodeId: string) => void;
|
||||
|
||||
// Edge operations
|
||||
setEdges: (edges: FlowEdge[] | ((edges: FlowEdge[]) => FlowEdge[])) => void;
|
||||
setEdges: (edges: FlowEdge[], changes?: EdgeChange<FlowEdge>[]) => void;
|
||||
addEdge: (edge: FlowEdge) => void;
|
||||
updateEdge: (edgeId: string, updates: Partial<FlowEdge>) => void;
|
||||
deleteEdge: (edgeId: string) => void;
|
||||
|
|
@ -78,7 +78,6 @@ interface WorkflowActions {
|
|||
// UI state
|
||||
setIsDirty: (isDirty: boolean) => void;
|
||||
setIsAddNodePanelOpen: (isOpen: boolean) => void;
|
||||
setIsEditingName: (isEditing: boolean) => void;
|
||||
|
||||
// Validation
|
||||
setWorkflowValidationErrors: (errors: WorkflowError[]) => void;
|
||||
|
|
@ -108,7 +107,6 @@ export const useWorkflowStore = create<WorkflowStore>((set, get) => ({
|
|||
historyIndex: -1,
|
||||
isDirty: false,
|
||||
isAddNodePanelOpen: false,
|
||||
isEditingName: false,
|
||||
workflowValidationErrors: [],
|
||||
templateContextVariables: {},
|
||||
workflowConfigurations: DEFAULT_WORKFLOW_CONFIGURATIONS,
|
||||
|
|
@ -194,19 +192,27 @@ export const useWorkflowStore = create<WorkflowStore>((set, get) => ({
|
|||
return state.historyIndex < state.history.length - 1;
|
||||
},
|
||||
|
||||
setNodes: (nodes) => {
|
||||
const state = get();
|
||||
let newNodes: FlowNode[];
|
||||
if (typeof nodes === 'function') {
|
||||
newNodes = nodes(state.nodes);
|
||||
setNodes: (nodes, changes) => {
|
||||
// Determine whether to push to history and set isDirty based on change types
|
||||
if (changes && changes.length > 0) {
|
||||
// Check if any changes are user-initiated (not just selections or dimensions)
|
||||
const hasDirtyChanges = changes.some(change =>
|
||||
change.type === 'add' ||
|
||||
change.type === 'remove' ||
|
||||
(change.type === 'position' && change.dragging)
|
||||
);
|
||||
|
||||
if (hasDirtyChanges) {
|
||||
get().pushToHistory();
|
||||
set({ nodes, isDirty: true });
|
||||
} else {
|
||||
// For selection changes or dimension updates, don't push to history
|
||||
set({ nodes });
|
||||
}
|
||||
} else {
|
||||
newNodes = nodes;
|
||||
// No changes provided, just update nodes without history
|
||||
set({ nodes });
|
||||
}
|
||||
|
||||
// Push current state to history before making changes
|
||||
get().pushToHistory();
|
||||
|
||||
set({ nodes: newNodes, isDirty: true });
|
||||
},
|
||||
|
||||
addNode: (node) => {
|
||||
|
|
@ -241,19 +247,27 @@ export const useWorkflowStore = create<WorkflowStore>((set, get) => ({
|
|||
});
|
||||
},
|
||||
|
||||
setEdges: (edges) => {
|
||||
const state = get();
|
||||
let newEdges: FlowEdge[];
|
||||
if (typeof edges === 'function') {
|
||||
newEdges = edges(state.edges);
|
||||
setEdges: (edges, changes) => {
|
||||
// Determine whether to push to history and set isDirty based on change types
|
||||
if (changes && changes.length > 0) {
|
||||
// Check if any changes are user-initiated (not just selections)
|
||||
const hasDirtyChanges = changes.some(change =>
|
||||
change.type === 'add' ||
|
||||
change.type === 'remove' ||
|
||||
change.type === 'replace'
|
||||
);
|
||||
|
||||
if (hasDirtyChanges) {
|
||||
get().pushToHistory();
|
||||
set({ edges, isDirty: true });
|
||||
} else {
|
||||
// For selection changes, don't push to history
|
||||
set({ edges });
|
||||
}
|
||||
} else {
|
||||
newEdges = edges;
|
||||
// No changes provided, just update edges without history
|
||||
set({ edges });
|
||||
}
|
||||
|
||||
// Push current state to history before making changes
|
||||
get().pushToHistory();
|
||||
|
||||
set({ edges: newEdges, isDirty: true });
|
||||
},
|
||||
|
||||
addEdge: (edge) => {
|
||||
|
|
@ -306,10 +320,6 @@ export const useWorkflowStore = create<WorkflowStore>((set, get) => ({
|
|||
set({ isAddNodePanelOpen });
|
||||
},
|
||||
|
||||
setIsEditingName: (isEditingName) => {
|
||||
set({ isEditingName });
|
||||
},
|
||||
|
||||
setWorkflowValidationErrors: (workflowValidationErrors) => {
|
||||
set({ workflowValidationErrors });
|
||||
},
|
||||
|
|
@ -362,7 +372,6 @@ export const useWorkflowStore = create<WorkflowStore>((set, get) => ({
|
|||
historyIndex: -1,
|
||||
isDirty: false,
|
||||
isAddNodePanelOpen: false,
|
||||
isEditingName: false,
|
||||
workflowValidationErrors: [],
|
||||
templateContextVariables: {},
|
||||
workflowConfigurations: DEFAULT_WORKFLOW_CONFIGURATIONS,
|
||||
|
|
@ -387,4 +396,4 @@ export const useUndoRedo = () => {
|
|||
const canRedo = useWorkflowStore((state) => state.canRedo());
|
||||
|
||||
return { undo, redo, canUndo, canRedo };
|
||||
};
|
||||
};
|
||||
50
ui/src/app/workflow/[workflowId]/utils/layoutNodes.ts
Normal file
50
ui/src/app/workflow/[workflowId]/utils/layoutNodes.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import dagre from '@dagrejs/dagre';
|
||||
import { ReactFlowInstance } from "@xyflow/react";
|
||||
|
||||
import { FlowEdge, FlowNode } from "@/components/flow/types";
|
||||
|
||||
export const layoutNodes = (
|
||||
nodes: FlowNode[],
|
||||
edges: FlowEdge[],
|
||||
rankdir: 'TB' | 'LR',
|
||||
rfInstance: React.RefObject<ReactFlowInstance<FlowNode, FlowEdge> | null>,
|
||||
saveWorkflow: (updateWorkflowDefinition: boolean) => Promise<void>
|
||||
) => {
|
||||
const g = new dagre.graphlib.Graph();
|
||||
g.setGraph({ rankdir, nodesep: 250, ranksep: 250 });
|
||||
g.setDefaultEdgeLabel(() => ({}));
|
||||
|
||||
// Sort nodes so startCall nodes come first and endCall nodes come last
|
||||
const sortedNodes = [...nodes].sort((a, b) => {
|
||||
if (a.type === 'startCall') return -1;
|
||||
if (b.type === 'startCall') return 1;
|
||||
if (a.type === 'endCall') return 1;
|
||||
if (b.type === 'endCall') return -1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
sortedNodes.forEach((node) => {
|
||||
g.setNode(node.id, { width: 180, height: 60 });
|
||||
});
|
||||
|
||||
edges.forEach((edge) => {
|
||||
g.setEdge(edge.source, edge.target);
|
||||
});
|
||||
|
||||
dagre.layout(g);
|
||||
|
||||
const newNodes = sortedNodes.map((node) => {
|
||||
const nodeWithPosition = g.node(node.id);
|
||||
return {
|
||||
...node,
|
||||
position: { x: nodeWithPosition.x, y: nodeWithPosition.y }
|
||||
};
|
||||
});
|
||||
|
||||
// Fit view to the new layout and save the viewport position
|
||||
setTimeout(() => {
|
||||
rfInstance.current?.fitView({ padding: 0.2, duration: 200, maxZoom: 0.75 });
|
||||
}, 0);
|
||||
|
||||
return newNodes;
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue