mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
fix(editor): Debug in Editor preserves binary data and prevents incorrect dirty marking (#18998)
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -48,6 +48,101 @@ describe('useExecutionDebugging()', () => {
|
||||
await expect(executionDebugging.applyExecutionData('1')).resolves.not.toThrowError();
|
||||
});
|
||||
|
||||
it('should pin binary data correctly during debug restoration', async () => {
|
||||
const mockExecution = {
|
||||
data: {
|
||||
resultData: {
|
||||
runData: {
|
||||
TriggerNode: [
|
||||
{
|
||||
data: {
|
||||
main: [
|
||||
[
|
||||
{
|
||||
json: { test: 'data' },
|
||||
binary: {
|
||||
data: {
|
||||
fileName: 'test.txt',
|
||||
mimeType: 'text/plain',
|
||||
data: 'dGVzdCBkYXRh',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as IExecutionResponse;
|
||||
|
||||
const workflowStore = mockedStore(useWorkflowsStore);
|
||||
workflowStore.getNodes.mockReturnValue([{ name: 'TriggerNode' }] as INodeUi[]);
|
||||
workflowStore.getExecution.mockResolvedValueOnce(mockExecution);
|
||||
workflowStore.workflowObject = {
|
||||
pinData: {},
|
||||
getParentNodes: vi.fn().mockReturnValue([]),
|
||||
} as unknown as Workflow;
|
||||
|
||||
await executionDebugging.applyExecutionData('1');
|
||||
|
||||
expect(workflowStore.pinData).toHaveBeenCalledWith({
|
||||
node: { name: 'TriggerNode' },
|
||||
data: [
|
||||
{
|
||||
json: { test: 'data' },
|
||||
binary: {
|
||||
data: {
|
||||
fileName: 'test.txt',
|
||||
mimeType: 'text/plain',
|
||||
data: 'dGVzdCBkYXRh',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
isRestoration: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle nodes with multiple main outputs during debug restoration', async () => {
|
||||
const mockExecution = {
|
||||
data: {
|
||||
resultData: {
|
||||
runData: {
|
||||
TriggerNode: [
|
||||
{
|
||||
data: {
|
||||
main: [
|
||||
[], // Empty first output
|
||||
[{ json: { test: 'data' } }], // Data in second output
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as IExecutionResponse;
|
||||
|
||||
const workflowStore = mockedStore(useWorkflowsStore);
|
||||
workflowStore.getNodes.mockReturnValue([{ name: 'TriggerNode' }] as INodeUi[]);
|
||||
workflowStore.getExecution.mockResolvedValueOnce(mockExecution);
|
||||
workflowStore.workflowObject = {
|
||||
pinData: {},
|
||||
getParentNodes: vi.fn().mockReturnValue([]),
|
||||
} as unknown as Workflow;
|
||||
|
||||
await executionDebugging.applyExecutionData('1');
|
||||
|
||||
expect(workflowStore.pinData).toHaveBeenCalledWith({
|
||||
node: { name: 'TriggerNode' },
|
||||
data: [{ json: { test: 'data' } }],
|
||||
isRestoration: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should show missing nodes warning toast', async () => {
|
||||
const mockExecution = {
|
||||
data: {
|
||||
|
||||
@@ -109,13 +109,18 @@ export const useExecutionDebugging = () => {
|
||||
let pinnings = 0;
|
||||
|
||||
pinnableNodes.forEach((node: INodeUi) => {
|
||||
const nodeData = runData[node.name]?.[0]?.data?.main?.[0];
|
||||
if (nodeData) {
|
||||
pinnings++;
|
||||
workflowsStore.pinData({
|
||||
node,
|
||||
data: nodeData,
|
||||
});
|
||||
const taskData = runData[node.name]?.[0];
|
||||
if (taskData?.data?.main) {
|
||||
// Get the first main output that has data, preserving all execution data including binary
|
||||
const nodeData = taskData.data.main.find((output) => output && output.length > 0);
|
||||
if (nodeData) {
|
||||
pinnings++;
|
||||
workflowsStore.pinData({
|
||||
node,
|
||||
data: nodeData,
|
||||
isRestoration: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -615,6 +615,67 @@ describe('useWorkflowsStore', () => {
|
||||
workflowsStore.pinData({ node, data });
|
||||
expect(uiStore.stateIsDirty).toBe(true);
|
||||
});
|
||||
|
||||
it('should preserve binary data when pinning', async () => {
|
||||
const node = { name: 'TestNode' } as INodeUi;
|
||||
const data = [
|
||||
{
|
||||
json: { test: 'data' },
|
||||
binary: {
|
||||
data: {
|
||||
fileName: 'test.txt',
|
||||
mimeType: 'text/plain',
|
||||
data: 'dGVzdCBkYXRh',
|
||||
},
|
||||
},
|
||||
},
|
||||
] as unknown as INodeExecutionData[];
|
||||
|
||||
workflowsStore.pinData({ node, data });
|
||||
|
||||
expect(workflowsStore.workflow.pinData?.[node.name]).toEqual([
|
||||
{
|
||||
json: { test: 'data' },
|
||||
binary: {
|
||||
data: {
|
||||
fileName: 'test.txt',
|
||||
mimeType: 'text/plain',
|
||||
data: 'dGVzdCBkYXRh',
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not update timestamp during restoration', async () => {
|
||||
const node = { name: 'TestNode' } as INodeUi;
|
||||
const data = [{ json: 'testData' }] as unknown as INodeExecutionData[];
|
||||
|
||||
// Set up existing pinned data with metadata
|
||||
workflowsStore.workflow.pinData = { [node.name]: data };
|
||||
workflowsStore.nodeMetadata[node.name] = { pristine: false, pinnedDataLastUpdatedAt: 1000 };
|
||||
|
||||
workflowsStore.pinData({ node, data, isRestoration: true });
|
||||
|
||||
expect(workflowsStore.nodeMetadata[node.name].pinnedDataLastUpdatedAt).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should clear timestamps during restoration', async () => {
|
||||
const node = { name: 'TestNode' } as INodeUi;
|
||||
const data = [{ json: 'testData' }] as unknown as INodeExecutionData[];
|
||||
|
||||
// Set up existing metadata with timestamps
|
||||
workflowsStore.nodeMetadata[node.name] = {
|
||||
pristine: false,
|
||||
pinnedDataLastUpdatedAt: 1000,
|
||||
pinnedDataLastRemovedAt: 2000,
|
||||
};
|
||||
|
||||
workflowsStore.pinData({ node, data, isRestoration: true });
|
||||
|
||||
expect(workflowsStore.nodeMetadata[node.name].pinnedDataLastUpdatedAt).toBeUndefined();
|
||||
expect(workflowsStore.nodeMetadata[node.name].pinnedDataLastRemovedAt).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateNodeExecutionData', () => {
|
||||
|
||||
@@ -989,7 +989,11 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
||||
);
|
||||
}
|
||||
|
||||
function pinData(payload: { node: INodeUi; data: INodeExecutionData[] }): void {
|
||||
function pinData(payload: {
|
||||
node: INodeUi;
|
||||
data: INodeExecutionData[];
|
||||
isRestoration?: boolean;
|
||||
}): void {
|
||||
const nodeName = payload.node.name;
|
||||
|
||||
if (!workflow.value.pinData) {
|
||||
@@ -1000,13 +1004,23 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
||||
payload.data = [payload.data];
|
||||
}
|
||||
|
||||
if ((workflow.value.pinData?.[nodeName] ?? []).length > 0 && nodeMetadata.value[nodeName]) {
|
||||
if (
|
||||
(workflow.value.pinData?.[nodeName] ?? []).length > 0 &&
|
||||
nodeMetadata.value[nodeName] &&
|
||||
!payload.isRestoration
|
||||
) {
|
||||
// Updating existing pinned data
|
||||
nodeMetadata.value[nodeName].pinnedDataLastUpdatedAt = Date.now();
|
||||
} else if (payload.isRestoration && nodeMetadata.value[nodeName]) {
|
||||
// Clear timestamps during restoration to prevent incorrect dirty marking
|
||||
delete nodeMetadata.value[nodeName].pinnedDataLastUpdatedAt;
|
||||
delete nodeMetadata.value[nodeName].pinnedDataLastRemovedAt;
|
||||
}
|
||||
|
||||
const storedPinData = payload.data.map((item) =>
|
||||
isJsonKeyObject(item) ? { json: item.json } : { json: item },
|
||||
isJsonKeyObject(item)
|
||||
? { json: item.json, ...(item.binary && { binary: item.binary }) }
|
||||
: { json: item },
|
||||
);
|
||||
|
||||
workflow.value.pinData[nodeName] = storedPinData;
|
||||
|
||||
Reference in New Issue
Block a user