mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +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();
|
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 () => {
|
it('should show missing nodes warning toast', async () => {
|
||||||
const mockExecution = {
|
const mockExecution = {
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -109,13 +109,18 @@ export const useExecutionDebugging = () => {
|
|||||||
let pinnings = 0;
|
let pinnings = 0;
|
||||||
|
|
||||||
pinnableNodes.forEach((node: INodeUi) => {
|
pinnableNodes.forEach((node: INodeUi) => {
|
||||||
const nodeData = runData[node.name]?.[0]?.data?.main?.[0];
|
const taskData = runData[node.name]?.[0];
|
||||||
if (nodeData) {
|
if (taskData?.data?.main) {
|
||||||
pinnings++;
|
// Get the first main output that has data, preserving all execution data including binary
|
||||||
workflowsStore.pinData({
|
const nodeData = taskData.data.main.find((output) => output && output.length > 0);
|
||||||
node,
|
if (nodeData) {
|
||||||
data: nodeData,
|
pinnings++;
|
||||||
});
|
workflowsStore.pinData({
|
||||||
|
node,
|
||||||
|
data: nodeData,
|
||||||
|
isRestoration: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -615,6 +615,67 @@ describe('useWorkflowsStore', () => {
|
|||||||
workflowsStore.pinData({ node, data });
|
workflowsStore.pinData({ node, data });
|
||||||
expect(uiStore.stateIsDirty).toBe(true);
|
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', () => {
|
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;
|
const nodeName = payload.node.name;
|
||||||
|
|
||||||
if (!workflow.value.pinData) {
|
if (!workflow.value.pinData) {
|
||||||
@@ -1000,13 +1004,23 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
|||||||
payload.data = [payload.data];
|
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
|
// Updating existing pinned data
|
||||||
nodeMetadata.value[nodeName].pinnedDataLastUpdatedAt = Date.now();
|
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) =>
|
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;
|
workflow.value.pinData[nodeName] = storedPinData;
|
||||||
|
|||||||
Reference in New Issue
Block a user