Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/nodes-palette-race-condition.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@workflowbuilder/sdk': patch
---

Simultaneous loading of nodes and the palette can result in a race condition. If nodes are loaded before the palette, new errors (for example, for fields that were not evaluated earlier) are skipped, and the node only shows a “!” indicator when the user selects it.
25 changes: 24 additions & 1 deletion packages/sdk/src/store/slices/diagram-slice/actions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
// About actions: apps/demo/src/app/store/README.md
import { isDeepEqual } from 'remeda';

import {
migrateLegacyHandleIdsOnEdges,
migrateLegacyHandleIdsOnNodes,
Expand All @@ -8,7 +10,7 @@ import type { VariableDefinition } from '../../../features/variables/types';
import type { LayoutDirection } from '../../../node/common';
import type { WorkflowBuilderEdge, WorkflowBuilderNode } from '../../../node/node-data';
import type { IntegrationDataFormat } from '../../../types/integration';
import { getNodeWithErrors } from '../../../utils/validation/get-node-errors';
import { getNodeWithErrors, getNodesWithErrors } from '../../../utils/validation/get-node-errors';
import { useStore } from '../../store';
import { skipDynamicValuesInEdges, skipDynamicValuesInNodes } from './utils/dynamic-values';

Expand Down Expand Up @@ -122,6 +124,27 @@ export function getStoreSingleSelected() {
return selectSingleSelectedElement(state);
}

/**
* Revalidates all nodes in the store and updates their JSON schema validation errors if needed.
*
* Compares current nodes with revalidated ones and only updates state when errors change,
* avoiding unnecessary re-renders.
*
* @category Store
*/
export function refreshNodesErrorsIfNeeded() {
const stateNodes = useStore.getState().nodes;
const stateNodesWithRefreshedErrors = getNodesWithErrors(stateNodes);

if (isDeepEqual(stateNodes, stateNodesWithRefreshedErrors)) {
return;
}

useStore.setState({
nodes: stateNodesWithRefreshedErrors,
});
}

export function saveVariableDefinition(definition: VariableDefinition) {
useStore.setState((state) => ({
globalVariables: {
Expand Down
4 changes: 4 additions & 0 deletions packages/sdk/src/store/slices/palette/palette-slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
StatusType,
} from '../../../node/common';
import type { GetDiagramState, SetDiagramState } from '../../store';
import { refreshNodesErrorsIfNeeded } from '../diagram-slice/actions';

export type PaletteState = {
isSidebarExpanded: boolean;
Expand Down Expand Up @@ -40,6 +41,9 @@ export function usePaletteSlice(set: SetDiagramState, get: GetDiagramState): Pal
data: getPaletteData(),
fetchDataStatus: StatusType.Success,
});

// Set a timeout to postpone to next event loop iteration
setTimeout(() => refreshNodesErrorsIfNeeded(), 1);
},
getNodeDefinition: (nodeType) => {
const { data } = get();
Expand Down
4 changes: 4 additions & 0 deletions packages/sdk/src/utils/validation/get-node-errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,7 @@ export function getNodeWithErrors(node: WorkflowBuilderNode) {
},
};
}

export function getNodesWithErrors(nodes: WorkflowBuilderNode[]): WorkflowBuilderNode[] {
return nodes.map(getNodeWithErrors);
}
Loading