Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
8d13f2b
5685 Add a resource reference type to property definitions
ivicac Sep 9, 2026
5fadc12
5685 Mark the data table properties as internal resource references
ivicac Sep 9, 2026
dbfab84
5685 Mark the knowledge base properties as internal resource references
ivicac Sep 9, 2026
a7198c7
5685 Add the internal resource reference validator and its resolver c…
ivicac Sep 9, 2026
284f2d5
5685 Resolve data table and knowledge base references
ivicac Sep 9, 2026
81c34e2
5685 Keep the outputs of the later nodes when one node's dynamic outp…
ivicac Sep 9, 2026
d7e362e
5685 Stop the validator from reporting properties the editor adds, hi…
ivicac Sep 9, 2026
b3ec598
5685 Check resource references and cluster elements while validating …
ivicac Sep 9, 2026
a045df8
5685 Report validation issues per node over GraphQL
ivicac Sep 9, 2026
f11d47c
5685 client - Collect workflow issues from the validator, the lookups…
ivicac Sep 9, 2026
6bc109b
5685 client - Show workflow issues on the canvas nodes and in a note
ivicac Sep 9, 2026
3981c90
5685 client - Add the workflow issues sidebar
ivicac Sep 9, 2026
0e58578
5685 client - Record node lookup failures as node issues instead of t…
ivicac Sep 9, 2026
900c18a
5685 client - Show the workflow errors and warnings in the code editor
ivicac Sep 9, 2026
c8a081f
5685 client - Match the left sidebar slide easing on the project and …
ivicac Sep 9, 2026
3245042
5685 Regenerate the knowledge base component definition
ivicac Sep 9, 2026
78f901c
5685 client - Regenerate the GraphQL middleware
ivicac Sep 9, 2026
12a7391
5685 client - Pan the viewport for right-hand overlay panels instead …
ivicac Sep 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions client/src/config/tests/useFetchInterceptor.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import useWorkflowIssuesStore from '@/pages/platform/workflow-editor/stores/useWorkflowIssuesStore';
import {act, renderHook} from '@testing-library/react';
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';

Expand Down Expand Up @@ -669,4 +670,45 @@ describe('useFetchInterceptor', () => {
expect(innerFetch).toHaveBeenCalledTimes(1);
});
});

describe('workflow node lookups', () => {
const lookupUrl = 'http://localhost/internal/workflows/1052/workflow-nodes/dataTable_2/options/table';

beforeEach(() => {
useWorkflowIssuesStore.getState().reset();
});

it('records a failed lookup against the node instead of toasting', async () => {
renderHook(() => useFetchInterceptor());

const response = createMockResponse({
jsonData: {detail: "Table does not have primary key column 'id': dt_0_conversations", title: 'Error'},
status: 500,
url: lookupUrl,
});

await act(async () => {
hoisted.registeredHandlers!.response(response);

await Promise.resolve();
});

expect(hoisted.toastError).not.toHaveBeenCalled();
expect(useWorkflowIssuesStore.getState().liveIssues['dataTable_2|table|LOOKUP_FAILED'].message).toBe(
"Table does not have primary key column 'id': dt_0_conversations"
);
});

it('clears the recorded failure when the same lookup succeeds', async () => {
useWorkflowIssuesStore.getState().recordLookupFailure('dataTable_2', 'table', 'stale');

renderHook(() => useFetchInterceptor());

await act(async () => {
hoisted.registeredHandlers!.response(createMockResponse({status: 200, url: lookupUrl}));
});

expect(useWorkflowIssuesStore.getState().liveIssues).toEqual({});
});
});
});
5 changes: 5 additions & 0 deletions client/src/config/useFetchInterceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {useWorkspaceStore} from '@/pages/automation/stores/useWorkspaceStore';
import {buildLoginPath} from '@/shared/auth/login-redirect-utils';
import {useAuthenticationStore} from '@/shared/stores/useAuthenticationStore';
import {getCookie} from '@/shared/util/cookie-utils';
import recordWorkflowNodeLookupResult from '@/shared/util/recordWorkflowNodeLookupResult';
import fetchIntercept from 'fetch-intercept';
import {useEffect, useRef} from 'react';
import {useNavigate} from 'react-router-dom';
Expand Down Expand Up @@ -168,6 +169,10 @@ export default function useFetchInterceptor() {
return response;
}

if (recordWorkflowNodeLookupResult(response)) {
return response;
}

// Endpoints that surface their own error state inline shouldn't also flash a
// global toast.
if ((response.status < 200 || response.status > 299) && handlesErrorInline(response.url)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ const AutomationWorkflow = () => {
<div className="h-full shrink-0 overflow-hidden">
<div
className={twMerge(
'h-full w-[355px] transition-[margin-left,opacity] duration-300 ease-out',
'h-full w-[355px] transition-[margin-left,opacity] duration-300 ease-[cubic-bezier(0.33,1,0.68,1)]',
leftSidebarOpen ? 'ml-0 opacity-100' : 'ml-[-355px] opacity-0'
)}
>
Expand Down
2 changes: 1 addition & 1 deletion client/src/ee/pages/embedded/integration/Integration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ const Integration = () => {
<div className="h-full shrink-0 overflow-hidden">
<div
className={twMerge(
'h-full w-[355px] transition-[margin-left,opacity] duration-300 ease-out',
'h-full w-[355px] transition-[margin-left,opacity] duration-300 ease-[cubic-bezier(0.33,1,0.68,1)]',
leftSidebarOpen ? 'ml-0 opacity-100' : 'ml-[-355px] opacity-0'
)}
>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import useWorkflowIssuesStore from '@/pages/platform/workflow-editor/stores/useWorkflowIssuesStore';
import {act, renderHook} from '@testing-library/react';
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';

Expand Down Expand Up @@ -267,4 +268,45 @@ describe('useFetchInterceptor (embedded)', () => {
});
});
});

describe('workflow node lookups', () => {
const lookupUrl = 'http://localhost/internal/workflows/1052/workflow-nodes/dataTable_2/options/table';

beforeEach(() => {
useWorkflowIssuesStore.getState().reset();
});

it('records a failed lookup against the node instead of toasting', async () => {
renderHook(() => useFetchInterceptor());

const response = createMockResponse({
jsonData: {detail: "Table does not have primary key column 'id': dt_0_conversations", title: 'Error'},
status: 500,
url: lookupUrl,
});

await act(async () => {
hoisted.registeredHandlers!.response(response);

await Promise.resolve();
});

expect(hoisted.toastError).not.toHaveBeenCalled();
expect(useWorkflowIssuesStore.getState().liveIssues['dataTable_2|table|LOOKUP_FAILED'].message).toBe(
"Table does not have primary key column 'id': dt_0_conversations"
);
});

it('clears the recorded failure when the same lookup succeeds', async () => {
useWorkflowIssuesStore.getState().recordLookupFailure('dataTable_2', 'table', 'stale');

renderHook(() => useFetchInterceptor());

await act(async () => {
hoisted.registeredHandlers!.response(createMockResponse({status: 200, url: lookupUrl}));
});

expect(useWorkflowIssuesStore.getState().liveIssues).toEqual({});
});
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {useWorkspaceStore} from '@/pages/automation/stores/useWorkspaceStore';
import {useAuthenticationStore} from '@/shared/stores/useAuthenticationStore';
import {useEnvironmentStore} from '@/shared/stores/useEnvironmentStore';
import recordWorkflowNodeLookupResult from '@/shared/util/recordWorkflowNodeLookupResult';
import fetchIntercept from 'fetch-intercept';
import {useEffect, useRef} from 'react';
import {toast} from 'sonner';
Expand Down Expand Up @@ -86,6 +87,10 @@ export default function useFetchInterceptor() {
return response;
}

if (recordWorkflowNodeLookupResult(response)) {
return response;
}

const toastId = `fetch-error-${response.status}`;

if (response.url.includes('/graphql')) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
query ValidateWorkflow($workflowDefinition: String!) {
validateWorkflow(workflow: $workflowDefinition) {
query ValidateWorkflow($workflowDefinition: String!, $environmentId: Long) {
validateWorkflow(workflow: $workflowDefinition, environmentId: $environmentId) {
errors
warnings
nodeIssues {
nodeName
propertyPath
kind
severity
message
}
}
}
2 changes: 1 addition & 1 deletion client/src/pages/automation/project/Project.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ const Project = () => {
<div className="h-full shrink-0 overflow-hidden">
<div
className={twMerge(
'h-full w-[355px] transition-[margin-left,opacity] duration-300 ease-out',
'h-full w-[355px] transition-[margin-left,opacity] duration-300 ease-[cubic-bezier(0.33,1,0.68,1)]',
projectLeftSidebarOpen ? 'ml-0 opacity-100' : 'ml-[-355px] opacity-0'
)}
>
Expand Down
75 changes: 35 additions & 40 deletions client/src/pages/platform/workflow-editor/WorkflowEditorLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,29 @@ import './WorkflowEditorLayout.css';
import ClusterElementsCanvasDialog from '@/pages/platform/workflow-editor/components/ClusterElementsCanvasDialog';
import WorkflowNodeDetailsPanel from '@/pages/platform/workflow-editor/components/WorkflowNodeDetailsPanel';
import WorkflowTestChatPanel from '@/pages/platform/workflow-editor/components/workflow-test-chat/WorkflowTestChatPanel';
import useDelayedUnmount from '@/pages/platform/workflow-editor/hooks/useDelayedUnmount';
import useWorkflowEditorLayout from '@/pages/platform/workflow-editor/hooks/useWorkflowEditorLayout';
import useWorkflowIssues from '@/pages/platform/workflow-editor/hooks/useWorkflowIssues';
import useWorkflowIssuesSweep from '@/pages/platform/workflow-editor/hooks/useWorkflowIssuesSweep';
import useWorkflowIssuesValidation from '@/pages/platform/workflow-editor/hooks/useWorkflowIssuesValidation';
import {useWorkflowLayout} from '@/pages/platform/workflow-editor/hooks/useWorkflowLayout';
import {useWorkflowEditor} from '@/pages/platform/workflow-editor/providers/workflowEditorProvider';
import useRightSidebarStore from '@/pages/platform/workflow-editor/stores/useRightSidebarStore';
import useWorkflowEditorStore from '@/pages/platform/workflow-editor/stores/useWorkflowEditorStore';
import useWorkflowIssuesStore from '@/pages/platform/workflow-editor/stores/useWorkflowIssuesStore';
import useCopilotLayoutShifted from '@/shared/components/copilot/hooks/useCopilotLayoutShifted';
import useCopilotPanelStore from '@/shared/components/copilot/stores/useCopilotPanelStore';
import useCopilotPostTurnRegistry from '@/shared/components/copilot/stores/useCopilotPostTurnRegistry';
import useCopilotStateContributorRegistry from '@/shared/components/copilot/stores/useCopilotStateContributorRegistry';
import {Source, useCopilotStore} from '@/shared/components/copilot/stores/useCopilotStore';
import {ISSUES_SIDEBAR_EXIT_DURATION} from '@/shared/constants';
import {ProjectWorkflowKeys} from '@/shared/queries/automation/projectWorkflows.queries';
import {useQueryClient} from '@tanstack/react-query';
import {Suspense, lazy, useEffect, useState} from 'react';
import {useParams} from 'react-router-dom';
import {twMerge} from 'tailwind-merge';
import {useShallow} from 'zustand/shallow';

import ErrorsBanner from './components/ErrorsBanner';
import SubflowBanner from './components/SubflowBanner';
import WorkflowCodeEditorSheet from './components/WorkflowCodeEditorSheet';
import {
Expand All @@ -39,6 +44,7 @@ import {clearAllWorkflowMutations} from './utils/workflowMutationGuard';

const DataPillPanel = lazy(() => import('./components/datapills/DataPillPanel'));
const WorkflowEditor = lazy(() => import('./components/WorkflowEditor'));
const WorkflowIssuesSidebar = lazy(() => import('./components/WorkflowIssuesSidebar'));
const WorkflowRightSidebar = lazy(() => import('./components/WorkflowRightSidebar'));
const WorkflowNodesSidebar = lazy(() => import('./components/WorkflowNodesSidebar'));

Expand All @@ -64,14 +70,16 @@ const WorkflowEditorLayout = ({
workflowReferenceId,
}: WorkflowEditorLayoutProps) => {
const [clusterDialogMounted, setClusterDialogMounted] = useState(false);
const [rightSidebarMounted, setRightSidebarMounted] = useState(false);
const [rightSidebarVisible, setRightSidebarVisible] = useState(false);

const copilotLayoutShifted = useCopilotLayoutShifted();
const copilotPanelOpen = useCopilotPanelStore((state) => state.copilotPanelOpen);
const rightSidebarOpen = useRightSidebarStore((state) => state.rightSidebarOpen);
const workflow = useWorkflowDataStore((state) => state.workflow);
const currentNode = useWorkflowNodeDetailsPanelStore((state) => state.currentNode);
const issuesSidebarOpen = useWorkflowIssuesStore((state) => state.issuesSidebarOpen);
const workflowNodeDetailsPanelOpen = useWorkflowNodeDetailsPanelStore(
(state) => state.workflowNodeDetailsPanelOpen
);
const {
clusterElementsCanvasOpen,
setShowWorkflowCodeEditorSheet,
Expand Down Expand Up @@ -100,6 +108,7 @@ const WorkflowEditorLayout = ({
handleCopilotClick,
handleWorkflowCodeEditorClick,
handleWorkflowInputsClick,
handleWorkflowIssuesClick,
handleWorkflowOutputsClick,
isWorkflowNodeOutputsPending,
previousComponentDefinitions,
Expand All @@ -108,12 +117,23 @@ const WorkflowEditorLayout = ({
workflowTestConfiguration,
} = useWorkflowLayout(includeComponents);

useWorkflowIssuesSweep();
useWorkflowIssuesValidation();

const issues = useWorkflowIssues();

const {invalidateWorkflowQueries, updateWorkflowMutation} = useWorkflowEditor();
const {handleClusterElementsCanvasOpenChange, isMainRootClusterElement} = useWorkflowEditorLayout();

const queryClient = useQueryClient();
const {projectId, projectWorkflowId} = useParams();

const {mounted: rightSidebarMounted, visible: rightSidebarVisible} = useDelayedUnmount(rightSidebarOpen);
const {mounted: issuesSidebarMounted, visible: issuesSidebarVisible} = useDelayedUnmount(
issuesSidebarOpen,
workflowNodeDetailsPanelOpen ? 0 : ISSUES_SIDEBAR_EXIT_DURATION
);

useEffect(() => {
return useCopilotStateContributorRegistry.getState().register(() => {
const activeWorkflow = useWorkflowDataStore.getState().workflow;
Expand All @@ -139,40 +159,6 @@ const WorkflowEditorLayout = ({
});
}, [projectId, projectWorkflowId, queryClient]);

useEffect(() => {
let outerRafId: number | undefined;
let innerRafId: number | undefined;
let timerId: ReturnType<typeof setTimeout> | undefined;

if (rightSidebarOpen) {
setRightSidebarMounted(true);

outerRafId = requestAnimationFrame(() => {
innerRafId = requestAnimationFrame(() => {
setRightSidebarVisible(true);
});
});
} else {
setRightSidebarVisible(false);

timerId = setTimeout(() => setRightSidebarMounted(false), 300);
}

return () => {
if (outerRafId !== undefined) {
cancelAnimationFrame(outerRafId);
}

if (innerRafId !== undefined) {
cancelAnimationFrame(innerRafId);
}

if (timerId !== undefined) {
clearTimeout(timerId);
}
};
}, [rightSidebarOpen]);

useEffect(() => {
if (clusterElementsCanvasOpen) {
setClusterDialogMounted(true);
Expand All @@ -188,6 +174,7 @@ const WorkflowEditorLayout = ({
clearAllWorkflowMutations();

useWorkflowNodeDetailsPanelStore.getState().clearPendingSaveNodeNames();
useWorkflowIssuesStore.getState().reset();
};
}, []);

Expand All @@ -202,8 +189,6 @@ const WorkflowEditorLayout = ({
>
<div className="absolute top-2 left-2 z-10 flex flex-col gap-2">
<SubflowBanner />

<ErrorsBanner />
</div>

{componentDefinitions && taskDispatcherDefinitions && (
Expand All @@ -229,18 +214,28 @@ const WorkflowEditorLayout = ({
</Suspense>
)}

{issuesSidebarMounted && (
<Suspense>
<WorkflowIssuesSidebar visible={issuesSidebarVisible} />
</Suspense>
)}

{componentDefinitions && taskDispatcherDefinitions && (
<Suspense
fallback={
<WorkflowRightSidebarSkeleton itemCount={!showCopilot && !showWorkflowInputs ? 2 : 4} />
<WorkflowRightSidebarSkeleton itemCount={!showCopilot && !showWorkflowInputs ? 3 : 5} />
}
>
<WorkflowRightSidebar
copilotPanelOpen={copilotPanelOpen}
issueCount={issues.length}
issueSeverity={issues[0]?.severity}
issuesSidebarOpen={issuesSidebarOpen}
onComponentsAndFlowControlsClick={handleComponentsAndFlowControlsClick}
onCopilotClick={handleCopilotClick}
onWorkflowCodeEditorClick={handleWorkflowCodeEditorClick}
onWorkflowInputsClick={handleWorkflowInputsClick}
onWorkflowIssuesClick={handleWorkflowIssuesClick}
onWorkflowOutputsClick={handleWorkflowOutputsClick}
rightSidebarOpen={rightSidebarOpen}
showCopilot={showCopilot}
Expand Down
Loading
Loading