@@ -220,6 +224,28 @@ function openContextMenu(event: MouseEvent) {
}
}
+ /**
+ * Diff classes
+ */
+
+ &.diffAdded {
+ border-color: green;
+ border-style: dashed;
+ box-shadow: 0 0 var(--spacing-xs) 0 rgba(0, 255, 0, 0.25);
+ }
+
+ &.diffDeleted {
+ border-color: red;
+ border-style: dashed;
+ box-shadow: 0 0 var(--spacing-xs) 0 rgba(255, 0, 0, 0.25);
+ }
+
+ &.diffModified {
+ border-color: orange;
+ border-style: dashed;
+ box-shadow: 0 0 var(--spacing-xs) 0 rgba(255, 153, 0, 0.25);
+ }
+
/**
* State classes
* The reverse order defines the priority in case multiple states are active
@@ -293,6 +319,13 @@ function openContextMenu(event: MouseEvent) {
font-weight: 400;
}
+.diffStatus {
+ position: absolute;
+ top: 0;
+ left: 50%;
+ transform: translate(-50%, -50%);
+}
+
.statusIcons {
position: absolute;
bottom: var(--canvas-node--status-icons-offset);
diff --git a/packages/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeDiffStatus.vue b/packages/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeDiffStatus.vue
new file mode 100644
index 0000000000..a0b3810066
--- /dev/null
+++ b/packages/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeDiffStatus.vue
@@ -0,0 +1,47 @@
+
+
+
+ {{ label }}
+
+
+
diff --git a/packages/editor-ui/src/composables/useCanvasMapping.ts b/packages/editor-ui/src/composables/useCanvasMapping.ts
index 9858a2b930..44c4d49375 100644
--- a/packages/editor-ui/src/composables/useCanvasMapping.ts
+++ b/packages/editor-ui/src/composables/useCanvasMapping.ts
@@ -380,6 +380,8 @@ export function useCanvasMapping({
}, {}),
);
+ const nodeDiffById = computed(() => workflowsStore.workflowDiff || {});
+
const additionalNodePropertiesById = computed(() => {
type StickyNoteBoundingBox = BoundingBox & {
id: string;
@@ -459,6 +461,7 @@ export function useCanvasMapping({
disabled: node.disabled,
inputs: nodeInputsById.value[node.id] ?? [],
outputs: nodeOutputsById.value[node.id] ?? [],
+ diff: nodeDiffById.value[node.id],
connections: {
[CanvasConnectionMode.Input]: inputConnections,
[CanvasConnectionMode.Output]: outputConnections,
diff --git a/packages/editor-ui/src/composables/useCanvasNode.ts b/packages/editor-ui/src/composables/useCanvasNode.ts
index 56b27264ba..2e29912ffe 100644
--- a/packages/editor-ui/src/composables/useCanvasNode.ts
+++ b/packages/editor-ui/src/composables/useCanvasNode.ts
@@ -22,6 +22,7 @@ export function useCanvasNode() {
disabled: false,
inputs: [],
outputs: [],
+ diff: undefined,
connections: { [CanvasConnectionMode.Input]: {}, [CanvasConnectionMode.Output]: {} },
issues: { items: [], visible: false },
pinnedData: { count: 0, visible: false },
@@ -63,6 +64,8 @@ export function useCanvasNode() {
const runDataIterations = computed(() => data.value.runData.iterations);
const hasRunData = computed(() => data.value.runData.visible);
+ const diff = computed(() => data.value.diff);
+
const render = computed(() => data.value.render);
const eventBus = computed(() => node?.eventBus.value);
@@ -89,6 +92,7 @@ export function useCanvasNode() {
executionStatus,
executionWaiting,
executionRunning,
+ diff,
render,
eventBus,
};
diff --git a/packages/editor-ui/src/constants.ts b/packages/editor-ui/src/constants.ts
index 47eba36ee6..0750a96899 100644
--- a/packages/editor-ui/src/constants.ts
+++ b/packages/editor-ui/src/constants.ts
@@ -10,6 +10,7 @@ import type {
CanvasNodeInjectionData,
} from '@/types';
import type { InjectionKey } from 'vue';
+import type { WorkflowHistoryActionType } from '@/types/workflowHistory';
export const MAX_WORKFLOW_SIZE = 1024 * 1024 * 16; // Workflow size limit in bytes
export const MAX_EXPECTED_REQUEST_SIZE = 2048; // Expected maximum workflow request metadata (i.e. headers) size in bytes
@@ -878,3 +879,23 @@ export const CanvasNodeHandleKey =
export const BROWSER_ID_STORAGE_KEY = 'n8n-browserId';
export const APP_MODALS_ELEMENT_ID = 'app-modals';
+
+/**
+ * Workflow History
+ */
+
+export const workflowHistoryActionTypes: WorkflowHistoryActionType[] = [
+ 'showdiff',
+ 'closediff',
+ 'restore',
+ 'clone',
+ 'open',
+ 'download',
+];
+
+export const WORKFLOW_HISTORY_ACTIONS = workflowHistoryActionTypes.reduce(
+ (record, key) => ({ ...record, [key.toUpperCase()]: key }),
+ {} as {
+ [K in Uppercase
]: Lowercase;
+ },
+);
diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json
index ea1db4369f..6d76b998ab 100644
--- a/packages/editor-ui/src/plugins/i18n/locales/en.json
+++ b/packages/editor-ui/src/plugins/i18n/locales/en.json
@@ -2202,7 +2202,8 @@
"workflowHistory.item.actions.clone": "Clone to new workflow",
"workflowHistory.item.actions.open": "Open version in new tab",
"workflowHistory.item.actions.download": "Download",
- "workflowHistory.item.actions.diff": "Show workflow diff",
+ "workflowHistory.item.actions.showdiff": "Show workflow diff",
+ "workflowHistory.item.actions.closediff": "Close diff",
"workflowHistory.item.unsaved.title": "Unsaved version",
"workflowHistory.item.latest": "Latest saved",
"workflowHistory.empty": "No versions yet.",
@@ -2224,6 +2225,10 @@
"workflowHistory.button.tooltip.enabled": "Workflow history to view and restore previous versions of your workflows",
"workflowHistory.button.tooltip.disabled": "Upgrade to unlock workflow history to view and restore previous versions of your workflows. {link}",
"workflowHistory.button.tooltip.disabled.link": "View plans",
+ "diffStatus.added": "Added",
+ "diffStatus.modified": "Modified",
+ "diffStatus.deleted": "Deleted",
+ "diffStatus.equal": "Equal",
"workflows.heading": "Workflows",
"workflows.add": "Add workflow",
"workflows.project.add": "Add workflow to project",
diff --git a/packages/editor-ui/src/stores/workflows.store.ts b/packages/editor-ui/src/stores/workflows.store.ts
index a19e7d4042..7771715282 100644
--- a/packages/editor-ui/src/stores/workflows.store.ts
+++ b/packages/editor-ui/src/stores/workflows.store.ts
@@ -554,6 +554,8 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
activeExecutionId.value = null;
executingNode.value.length = 0;
executionWaitingForWebhook.value = false;
+
+ workflowDiff.value = undefined;
}
function addExecutingNode(nodeName: string) {
diff --git a/packages/editor-ui/src/types/canvas.ts b/packages/editor-ui/src/types/canvas.ts
index 3957e4d9c5..58c2502b25 100644
--- a/packages/editor-ui/src/types/canvas.ts
+++ b/packages/editor-ui/src/types/canvas.ts
@@ -10,6 +10,7 @@ import type { IExecutionResponse, INodeUi } from '@/Interface';
import type { ComputedRef, Ref } from 'vue';
import type { PartialBy } from '@/utils/typeHelpers';
import type { EventBus } from 'n8n-design-system';
+import { NodeDiff } from '@/types/workflowDiff.types';
export type CanvasConnectionPortType = NodeConnectionType;
@@ -86,6 +87,7 @@ export interface CanvasNodeData {
disabled: INodeUi['disabled'];
inputs: CanvasConnectionPort[];
outputs: CanvasConnectionPort[];
+ diff?: NodeDiff;
connections: {
[CanvasConnectionMode.Input]: INodeConnections;
[CanvasConnectionMode.Output]: INodeConnections;
diff --git a/packages/editor-ui/src/types/workflowDiff.types.ts b/packages/editor-ui/src/types/workflowDiff.types.ts
index 1eb2e55412..0828b8af5f 100644
--- a/packages/editor-ui/src/types/workflowDiff.types.ts
+++ b/packages/editor-ui/src/types/workflowDiff.types.ts
@@ -5,4 +5,8 @@ export const enum NodeDiffStatus {
Deleted = 'deleted',
}
-export type WorkflowDiff = Record;
+export type NodeDiff = {
+ status: NodeDiffStatus;
+};
+
+export type WorkflowDiff = Record;
diff --git a/packages/editor-ui/src/types/workflowHistory.ts b/packages/editor-ui/src/types/workflowHistory.ts
index c5753a4af9..6ac2c45a60 100644
--- a/packages/editor-ui/src/types/workflowHistory.ts
+++ b/packages/editor-ui/src/types/workflowHistory.ts
@@ -16,6 +16,12 @@ export type WorkflowVersion = WorkflowHistory & {
connections: IConnections;
};
-export type WorkflowHistoryActionType = 'restore' | 'clone' | 'open' | 'download' | 'diff';
+export type WorkflowHistoryActionType =
+ | 'restore'
+ | 'clone'
+ | 'open'
+ | 'download'
+ | 'showdiff'
+ | 'closediff';
export type WorkflowHistoryRequestParams = { take: number; skip?: number };
diff --git a/packages/editor-ui/src/utils/workflowDiff.ts b/packages/editor-ui/src/utils/workflowDiff.ts
index 6e3e14649f..666433ac42 100644
--- a/packages/editor-ui/src/utils/workflowDiff.ts
+++ b/packages/editor-ui/src/utils/workflowDiff.ts
@@ -1,8 +1,7 @@
import type { WorkflowDiff } from '@/types/workflowDiff.types';
-import type { INodeUi, IWorkflowDb } from '@/Interface';
import { NodeDiffStatus } from '@/types/workflowDiff.types';
-import _pick from 'lodash/pick';
-import _isEqual from 'lodash/isEqual';
+import type { INodeUi, IWorkflowDb } from '@/Interface';
+import { pick as _pick, isEqual as _isEqual } from 'lodash-es';
export function compareNodes(base: INodeUi, target: INodeUi): boolean {
const propsToCompare = ['name', 'type', 'typeVersion', 'webhookId', 'credentials', 'parameters'];
@@ -38,17 +37,17 @@ export function compareWorkflows(
Object.keys(baseNodes).forEach((id) => {
if (!targetNodes[id]) {
- diff[id] = NodeDiffStatus.Deleted;
+ diff[id] = { status: NodeDiffStatus.Deleted };
} else if (!nodesEqual(baseNodes[id], targetNodes[id])) {
- diff[id] = NodeDiffStatus.Modified;
+ diff[id] = { status: NodeDiffStatus.Modified };
} else {
- diff[id] = NodeDiffStatus.Eq;
+ diff[id] = { status: NodeDiffStatus.Eq };
}
});
Object.keys(targetNodes).forEach((id) => {
if (!baseNodes[id]) {
- diff[id] = NodeDiffStatus.Added;
+ diff[id] = { status: NodeDiffStatus.Added };
}
});
diff --git a/packages/editor-ui/src/views/NodeView.v2.vue b/packages/editor-ui/src/views/NodeView.v2.vue
index 9f4774083e..140f85d64d 100644
--- a/packages/editor-ui/src/views/NodeView.v2.vue
+++ b/packages/editor-ui/src/views/NodeView.v2.vue
@@ -1351,6 +1351,8 @@ async function onPostMessageReceived(messageEvent: MessageEvent) {
)) as ExecutionSummary;
} else if (json?.command === 'setDiff') {
workflowsStore.workflowDiff = json.diff;
+ } else if (json?.command === 'fitView') {
+ fitView();
}
} catch (e) {}
}
diff --git a/packages/editor-ui/src/views/WorkflowHistory.vue b/packages/editor-ui/src/views/WorkflowHistory.vue
index d1ae2d4117..946b207bd6 100644
--- a/packages/editor-ui/src/views/WorkflowHistory.vue
+++ b/packages/editor-ui/src/views/WorkflowHistory.vue
@@ -1,8 +1,13 @@