Skip to content

Commit dc73064

Browse files
authored
ux fixes/improvements for integrations (#132)
* better integration handling * ux fixes * focus on add
1 parent 82c28c7 commit dc73064

File tree

15 files changed

+655
-187
lines changed

15 files changed

+655
-187
lines changed

app/workflows/[workflowId]/page.tsx

Lines changed: 148 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { useAtom, useSetAtom } from "jotai";
3+
import { useAtom, useAtomValue, useSetAtom } from "jotai";
44
import { ChevronLeft, ChevronRight } from "lucide-react";
55
import Link from "next/link";
66
import { useSearchParams } from "next/navigation";
@@ -10,6 +10,12 @@ import { Button } from "@/components/ui/button";
1010
import { NodeConfigPanel } from "@/components/workflow/node-config-panel";
1111
import { useIsMobile } from "@/hooks/use-mobile";
1212
import { api } from "@/lib/api-client";
13+
import {
14+
integrationsAtom,
15+
integrationsLoadedAtom,
16+
integrationsVersionAtom,
17+
} from "@/lib/integrations-store";
18+
import type { IntegrationType } from "@/lib/types/integration";
1319
import {
1420
currentWorkflowIdAtom,
1521
currentWorkflowNameAtom,
@@ -25,18 +31,77 @@ import {
2531
nodesAtom,
2632
rightPanelWidthAtom,
2733
selectedExecutionIdAtom,
28-
selectedNodeAtom,
2934
triggerExecuteAtom,
3035
updateNodeDataAtom,
3136
type WorkflowNode,
3237
type WorkflowVisibility,
3338
workflowNotFoundAtom,
3439
} from "@/lib/workflow-store";
40+
import { findActionById } from "@/plugins";
3541

3642
type WorkflowPageProps = {
3743
params: Promise<{ workflowId: string }>;
3844
};
3945

46+
// System actions that need integrations (not in plugin registry)
47+
const SYSTEM_ACTION_INTEGRATIONS: Record<string, IntegrationType> = {
48+
"Database Query": "database",
49+
};
50+
51+
// Helper to get required integration type for an action
52+
function getRequiredIntegrationType(
53+
actionType: string
54+
): IntegrationType | undefined {
55+
const action = findActionById(actionType);
56+
return (
57+
(action?.integration as IntegrationType | undefined) ||
58+
SYSTEM_ACTION_INTEGRATIONS[actionType]
59+
);
60+
}
61+
62+
// Helper to check and fix a single node's integration
63+
type IntegrationFixResult = {
64+
nodeId: string;
65+
newIntegrationId: string | undefined;
66+
};
67+
68+
function checkNodeIntegration(
69+
node: WorkflowNode,
70+
allIntegrations: { id: string; type: string }[],
71+
validIntegrationIds: Set<string>
72+
): IntegrationFixResult | null {
73+
const actionType = node.data.config?.actionType as string | undefined;
74+
if (!actionType) {
75+
return null;
76+
}
77+
78+
const integrationType = getRequiredIntegrationType(actionType);
79+
if (!integrationType) {
80+
return null;
81+
}
82+
83+
const currentIntegrationId = node.data.config?.integrationId as
84+
| string
85+
| undefined;
86+
const hasValidIntegration =
87+
currentIntegrationId && validIntegrationIds.has(currentIntegrationId);
88+
89+
if (hasValidIntegration) {
90+
return null;
91+
}
92+
93+
// Find available integrations of this type
94+
const available = allIntegrations.filter((i) => i.type === integrationType);
95+
96+
if (available.length === 1) {
97+
return { nodeId: node.id, newIntegrationId: available[0].id };
98+
}
99+
if (available.length === 0 && currentIntegrationId) {
100+
return { nodeId: node.id, newIntegrationId: undefined };
101+
}
102+
return null;
103+
}
104+
40105
const WorkflowEditor = ({ params }: WorkflowPageProps) => {
41106
const { workflowId } = use(params);
42107
const searchParams = useSearchParams();
@@ -52,7 +117,6 @@ const WorkflowEditor = ({ params }: WorkflowPageProps) => {
52117
const setCurrentWorkflowId = useSetAtom(currentWorkflowIdAtom);
53118
const setCurrentWorkflowName = useSetAtom(currentWorkflowNameAtom);
54119
const updateNodeData = useSetAtom(updateNodeDataAtom);
55-
const setSelectedNodeId = useSetAtom(selectedNodeAtom);
56120
const setHasUnsavedChanges = useSetAtom(hasUnsavedChangesAtom);
57121
const [workflowNotFound, setWorkflowNotFound] = useAtom(workflowNotFoundAtom);
58122
const setTriggerExecute = useSetAtom(triggerExecuteAtom);
@@ -66,6 +130,9 @@ const WorkflowEditor = ({ params }: WorkflowPageProps) => {
66130
currentWorkflowVisibilityAtom
67131
);
68132
const setIsWorkflowOwner = useSetAtom(isWorkflowOwnerAtom);
133+
const setGlobalIntegrations = useSetAtom(integrationsAtom);
134+
const setIntegrationsLoaded = useSetAtom(integrationsLoadedAtom);
135+
const integrationsVersion = useAtomValue(integrationsVersionAtom);
69136

70137
// Panel width state for resizing
71138
const [panelWidth, setPanelWidth] = useState(30); // default percentage
@@ -236,17 +303,14 @@ const WorkflowEditor = ({ params }: WorkflowPageProps) => {
236303
try {
237304
const workflowData = await api.ai.generate(prompt);
238305

239-
setNodes(workflowData.nodes || []);
306+
// Clear selection on all nodes
307+
const nodesWithoutSelection = (workflowData.nodes || []).map(
308+
(node: WorkflowNode) => ({ ...node, selected: false })
309+
);
310+
setNodes(nodesWithoutSelection);
240311
setEdges(workflowData.edges || []);
241312
setCurrentWorkflowName(workflowData.name || "AI Generated Workflow");
242313

243-
const selectedNode = workflowData.nodes?.find(
244-
(n: { selected?: boolean }) => n.selected
245-
);
246-
if (selectedNode) {
247-
setSelectedNodeId(selectedNode.id);
248-
}
249-
250314
await api.workflow.update(workflowId, {
251315
name: workflowData.name,
252316
description: workflowData.description,
@@ -267,7 +331,6 @@ const WorkflowEditor = ({ params }: WorkflowPageProps) => {
267331
setCurrentWorkflowName,
268332
setNodes,
269333
setEdges,
270-
setSelectedNodeId,
271334
]
272335
);
273336

@@ -281,9 +344,10 @@ const WorkflowEditor = ({ params }: WorkflowPageProps) => {
281344
return;
282345
}
283346

284-
// Reset all node statuses to idle when loading from database
347+
// Reset node statuses to idle and clear selection when loading from database
285348
const nodesWithIdleStatus = workflow.nodes.map((node: WorkflowNode) => ({
286349
...node,
350+
selected: false,
287351
data: {
288352
...node.data,
289353
status: "idle" as const,
@@ -300,11 +364,6 @@ const WorkflowEditor = ({ params }: WorkflowPageProps) => {
300364
setIsWorkflowOwner(workflow.isOwner !== false); // Default to true if not set
301365
setHasUnsavedChanges(false);
302366
setWorkflowNotFound(false);
303-
304-
const selectedNode = workflow.nodes.find((n: WorkflowNode) => n.selected);
305-
if (selectedNode) {
306-
setSelectedNodeId(selectedNode.id);
307-
}
308367
} catch (error) {
309368
console.error("Failed to load workflow:", error);
310369
toast.error("Failed to load workflow");
@@ -319,9 +378,13 @@ const WorkflowEditor = ({ params }: WorkflowPageProps) => {
319378
setIsWorkflowOwner,
320379
setHasUnsavedChanges,
321380
setWorkflowNotFound,
322-
setSelectedNodeId,
323381
]);
324382

383+
// Track if we've already auto-fixed integrations for this workflow+version
384+
const lastAutoFixRef = useRef<{ workflowId: string; version: number } | null>(
385+
null
386+
);
387+
325388
useEffect(() => {
326389
const loadWorkflowData = async () => {
327390
const isGeneratingParam = searchParams?.get("generating") === "true";
@@ -357,6 +420,72 @@ const WorkflowEditor = ({ params }: WorkflowPageProps) => {
357420
loadExistingWorkflow,
358421
]);
359422

423+
// Auto-fix invalid/missing integrations on workflow load or when integrations change
424+
useEffect(() => {
425+
// Skip if no nodes or no workflow
426+
if (nodes.length === 0 || !currentWorkflowId) {
427+
return;
428+
}
429+
430+
// Skip if already checked for this workflow+version combination
431+
const lastFix = lastAutoFixRef.current;
432+
if (
433+
lastFix &&
434+
lastFix.workflowId === currentWorkflowId &&
435+
lastFix.version === integrationsVersion
436+
) {
437+
return;
438+
}
439+
440+
const autoFixIntegrations = async () => {
441+
try {
442+
const allIntegrations = await api.integration.getAll();
443+
setGlobalIntegrations(allIntegrations);
444+
setIntegrationsLoaded(true);
445+
446+
const validIds = new Set(allIntegrations.map((i) => i.id));
447+
const fixes = nodes
448+
.map((node) => checkNodeIntegration(node, allIntegrations, validIds))
449+
.filter((fix): fix is IntegrationFixResult => fix !== null);
450+
451+
for (const fix of fixes) {
452+
const node = nodes.find((n) => n.id === fix.nodeId);
453+
if (node) {
454+
updateNodeData({
455+
id: fix.nodeId,
456+
data: {
457+
config: {
458+
...node.data.config,
459+
integrationId: fix.newIntegrationId,
460+
},
461+
},
462+
});
463+
}
464+
}
465+
466+
lastAutoFixRef.current = {
467+
workflowId: currentWorkflowId,
468+
version: integrationsVersion,
469+
};
470+
if (fixes.length > 0) {
471+
setHasUnsavedChanges(true);
472+
}
473+
} catch (error) {
474+
console.error("Failed to auto-fix integrations:", error);
475+
}
476+
};
477+
478+
autoFixIntegrations();
479+
}, [
480+
nodes,
481+
currentWorkflowId,
482+
integrationsVersion,
483+
updateNodeData,
484+
setGlobalIntegrations,
485+
setIntegrationsLoaded,
486+
setHasUnsavedChanges,
487+
]);
488+
360489
// Keyboard shortcuts
361490
const handleSave = useCallback(async () => {
362491
if (!currentWorkflowId || isGenerating) {

0 commit comments

Comments
 (0)