refactor(frontend): synchronize hardcoded values with handle IDs in FlowEditor

### Changes
- Implemented `syncHardcodedValuesWithHandleIds` in `useNodeStore` to ensure hardcoded values remain consistent with handle IDs.
- Updated `useFlow` to call the new synchronization method for each custom node when nodes are added.

### Impact
These changes enhance data integrity within the FlowEditor by preventing inconsistencies between hardcoded values and their corresponding handle IDs, improving overall functionality and user experience.
This commit is contained in:
abhi1992002
2026-01-03 20:15:28 +05:30
parent 4e5af1677e
commit 0fe8fcb9aa
4 changed files with 157 additions and 0 deletions

View File

@@ -121,6 +121,14 @@ export const useFlow = () => {
if (customNodes.length > 0) {
useNodeStore.getState().setNodes([]);
addNodes(customNodes);
// Sync hardcoded values with handle IDs.
// If a keyvalue field has a key without a value, the backend omits it from hardcoded values.
// But if a handleId exists for that key, it causes inconsistency.
// This ensures hardcoded values stay in sync with handle IDs.
customNodes.forEach((node) => {
useNodeStore.getState().syncHardcodedValuesWithHandleIds(node.id);
});
}
}, [customNodes, addNodes]);

View File

@@ -13,6 +13,10 @@ import { useHistoryStore } from "./historyStore";
import { useEdgeStore } from "./edgeStore";
import { BlockUIType } from "../components/types";
import { pruneEmptyValues } from "@/lib/utils";
import {
ensurePathExists,
parseHandleIdToPath,
} from "@/components/renderers/input-renderer-2/helpers";
// Minimum movement (in pixels) required before logging position change to history
// Prevents spamming history with small movements when clicking on inputs inside blocks
@@ -62,6 +66,8 @@ type NodeStore = {
errors: { [key: string]: string },
) => void;
clearAllNodeErrors: () => void; // Add this
syncHardcodedValuesWithHandleIds: (nodeId: string) => void;
};
export const useNodeStore = create<NodeStore>((set, get) => ({
@@ -305,4 +311,35 @@ export const useNodeStore = create<NodeStore>((set, get) => ({
})),
}));
},
syncHardcodedValuesWithHandleIds: (nodeId: string) => {
const node = get().nodes.find((n) => n.id === nodeId);
if (!node) return;
const handleIds = useEdgeStore.getState().getAllHandleIdsOfANode(nodeId);
const additionalHandles = handleIds.filter((h) => h.includes("_#_"));
if (additionalHandles.length === 0) return;
const hardcodedValues = JSON.parse(
JSON.stringify(node.data.hardcodedValues || {}),
);
let modified = false;
additionalHandles.forEach((handleId) => {
const segments = parseHandleIdToPath(handleId);
if (ensurePathExists(hardcodedValues, segments)) {
modified = true;
}
});
if (modified) {
set((state) => ({
nodes: state.nodes.map((n) =>
n.id === nodeId ? { ...n, data: { ...n.data, hardcodedValues } } : n,
),
}));
}
},
}));

View File

@@ -14,6 +14,7 @@ import {
KEY_PAIR_FLAG,
OBJECT_FLAG,
} from "./constants";
import { PathSegment } from "./types";
export function updateUiOption<T extends Record<string, any>>(
uiSchema: T | undefined,
@@ -176,3 +177,108 @@ export function isCredentialFieldSchema(schema: any): boolean {
"credentials_provider" in schema
);
}
export function parseHandleIdToPath(handleId: string): PathSegment[] {
let cleanedId = cleanUpHandleId(handleId);
const segments: PathSegment[] = [];
const parts = cleanedId.split(/(_#_|_@_|_\$_|\.)/);
let currentType: "property" | "item" | "additional" | "normal" = "normal";
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
if (part === "_#_") {
currentType = "additional";
} else if (part === "_@_") {
currentType = "property";
} else if (part === "_$_") {
currentType = "item";
} else if (part === ".") {
currentType = "normal";
} else if (part) {
const isNumeric = /^\d+$/.test(part);
if (currentType === "item" && isNumeric) {
segments.push({
key: part,
type: "item",
index: parseInt(part, 10),
});
} else {
segments.push({
key: part,
type: currentType,
});
}
currentType = "normal";
}
}
return segments;
}
/**
* Ensure a path exists in an object, creating intermediate objects/arrays as needed
* Returns true if any modifications were made
*/
export function ensurePathExists(
obj: Record<string, any>,
segments: PathSegment[],
): boolean {
if (segments.length === 0) return false;
let current = obj;
let modified = false;
for (let i = 0; i < segments.length; i++) {
const segment = segments[i];
const isLast = i === segments.length - 1;
const nextSegment = segments[i + 1];
const getDefaultValue = () => {
if (isLast) {
return "";
}
if (nextSegment?.type === "item") {
return [];
}
return {};
};
if (segment.type === "item" && segment.index !== undefined) {
if (!Array.isArray(current)) {
return modified;
}
while (current.length <= segment.index) {
current.push(isLast ? "" : {});
modified = true;
}
if (!isLast) {
if (
current[segment.index] === undefined ||
current[segment.index] === null
) {
current[segment.index] = getDefaultValue();
modified = true;
}
current = current[segment.index];
}
} else {
if (!(segment.key in current)) {
current[segment.key] = getDefaultValue();
modified = true;
} else if (!isLast && current[segment.key] === undefined) {
current[segment.key] = getDefaultValue();
modified = true;
}
if (!isLast) {
current = current[segment.key];
}
}
}
return modified;
}

View File

@@ -7,3 +7,9 @@ export interface ExtendedFormContextType extends FormContextType {
showHandles?: boolean;
size?: "small" | "medium" | "large";
}
export type PathSegment = {
key: string;
type: "property" | "item" | "additional" | "normal";
index?: number;
};