fix(editor): Improve WorkflowDiffModal UI (#17862)

This commit is contained in:
Csaba Tuncsik
2025-08-05 12:50:46 +02:00
committed by GitHub
parent 22ca768c13
commit eca95f3432
2 changed files with 154 additions and 87 deletions

View File

@@ -340,7 +340,8 @@ const modifiers = [
<N8nIconButton <N8nIconButton
icon="arrow-left" icon="arrow-left"
type="secondary" type="secondary"
class="mr-xs" :class="[$style.backButton, 'mr-xs']"
icon-size="large"
@click="closeDialog" @click="closeDialog"
></N8nIconButton> ></N8nIconButton>
<N8nHeading tag="h1" size="xlarge"> <N8nHeading tag="h1" size="xlarge">
@@ -359,7 +360,7 @@ const modifiers = [
modifiers, modifiers,
}" }"
:popper-class="$style.popper" :popper-class="$style.popper"
class="mr-xs" class="mr-2xs"
@visible-change="setActiveTab" @visible-change="setActiveTab"
> >
<N8nButton type="secondary"> <N8nButton type="secondary">
@@ -397,7 +398,7 @@ const modifiers = [
@click.prevent="setSelectedDetailId(change.node.id, activeTab)" @click.prevent="setSelectedDetailId(change.node.id, activeTab)"
> >
<DiffBadge :type="change.status" /> <DiffBadge :type="change.status" />
<NodeIcon :node-type="change.type" :size="16" /> <NodeIcon :node-type="change.type" :size="16" class="ml-2xs mr-4xs" />
{{ change.node.name }} {{ change.node.name }}
</ElDropdownItem> </ElDropdownItem>
</ul> </ul>
@@ -418,7 +419,11 @@ const modifiers = [
setSelectedDetailId(change[1].connection.source?.id, activeTab) setSelectedDetailId(change[1].connection.source?.id, activeTab)
" "
> >
<NodeIcon :node-type="change[1].connection.sourceType" :size="16" /> <NodeIcon
:node-type="change[1].connection.sourceType"
:size="16"
class="ml-2xs mr-4xs"
/>
{{ change[1].connection.source?.name }} {{ change[1].connection.source?.name }}
</ElDropdownItem> </ElDropdownItem>
<div :class="$style.separator"></div> <div :class="$style.separator"></div>
@@ -432,7 +437,11 @@ const modifiers = [
setSelectedDetailId(change[1].connection.target?.id, activeTab) setSelectedDetailId(change[1].connection.target?.id, activeTab)
" "
> >
<NodeIcon :node-type="change[1].connection.targetType" :size="16" /> <NodeIcon
:node-type="change[1].connection.targetType"
:size="16"
class="ml-2xs mr-4xs"
/>
{{ change[1].connection.target?.name }} {{ change[1].connection.target?.name }}
</ElDropdownItem> </ElDropdownItem>
</ul> </ul>
@@ -459,6 +468,7 @@ const modifiers = [
<N8nIconButton <N8nIconButton
icon="chevron-left" icon="chevron-left"
type="secondary" type="secondary"
class="mr-2xs"
:class="$style.navigationButton" :class="$style.navigationButton"
@click="previousNodeChange" @click="previousNodeChange"
></N8nIconButton> ></N8nIconButton>
@@ -507,11 +517,11 @@ const modifiers = [
<template v-else> <template v-else>
<div :class="$style.emptyWorkflow"> <div :class="$style.emptyWorkflow">
<template v-if="targetWorkFlow.state.value?.remote"> <template v-if="targetWorkFlow.state.value?.remote">
<N8nText color="text-dark" size="large"> Deleted workflow </N8nText> <N8nHeading size="large"> Deleted workflow </N8nHeading>
<N8nText color="text-base"> The workflow was deleted on the database </N8nText> <N8nText color="text-base"> The workflow was deleted on the database </N8nText>
</template> </template>
<template v-else> <template v-else>
<N8nText color="text-dark" size="large"> Deleted workflow </N8nText> <N8nHeading size="large"> Deleted workflow </N8nHeading>
<N8nText color="text-base"> The workflow was deleted on remote </N8nText> <N8nText color="text-base"> The workflow was deleted on remote </N8nText>
</template> </template>
</div> </div>
@@ -551,11 +561,11 @@ const modifiers = [
<template v-else> <template v-else>
<div :class="$style.emptyWorkflow"> <div :class="$style.emptyWorkflow">
<template v-if="targetWorkFlow.state.value?.remote"> <template v-if="targetWorkFlow.state.value?.remote">
<N8nText color="text-dark" size="large"> Deleted workflow </N8nText> <N8nHeading size="large"> Deleted workflow </N8nHeading>
<N8nText color="text-base"> The workflow was deleted on remote </N8nText> <N8nText color="text-base"> The workflow was deleted on remote </N8nText>
</template> </template>
<template v-else> <template v-else>
<N8nText color="text-dark" size="large"> Deleted workflow </N8nText> <N8nHeading size="large"> Deleted workflow </N8nHeading>
<N8nText color="text-base"> The workflow was deleted on the data base </N8nText> <N8nText color="text-base"> The workflow was deleted on the data base </N8nText>
</template> </template>
</div> </div>
@@ -577,7 +587,34 @@ const modifiers = [
</Modal> </Modal>
</template> </template>
<style module> <style module lang="scss">
/* Light theme diff colors */
:root,
[data-theme='light'] {
--diff-new: #0eab54;
--diff-new-light: #b4efc4;
--diff-new-faint: #ddfbe7;
--diff-modified: #bf941f;
--diff-modified-light: #f3dca1;
--diff-modified-faint: #fbf1d4;
--diff-del: #f51f32;
--diff-del-light: #fad3d0;
--diff-del-faint: #ffedec;
}
/* Dark theme diff colors */
[data-theme='dark'] {
--diff-new: #38cb7a;
--diff-new-light: #43674f;
--diff-new-faint: #3a463e;
--diff-modified: #d6a625;
--diff-modified-light: #6a5c38;
--diff-modified-faint: #464236;
--diff-del: #fb887a;
--diff-del-light: #7a524e;
--diff-del-faint: #4d3e3d;
}
.workflowDiffModal { .workflowDiffModal {
margin-bottom: 0; margin-bottom: 0;
border-radius: 0; border-radius: 0;
@@ -587,7 +624,7 @@ const modifiers = [
} }
:global(.el-dialog__header) { :global(.el-dialog__header) {
padding: 11px 16px; padding: 11px 16px;
border-bottom: 1px solid var(--color-border); border-bottom: 1px solid var(--color-foreground-base);
} }
:global(.el-dialog__headerbtn) { :global(.el-dialog__headerbtn) {
display: none; display: none;
@@ -629,50 +666,58 @@ const modifiers = [
} }
.changes { .changes {
list-style: none;
margin: 0;
padding: 0;
> li { > li {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
gap: 8px; gap: var(--spacing-2xs);
padding: 8px; padding: 10px 0 var(--spacing-3xs) var(--spacing-2xs);
ul {
margin-top: -3px;
} }
.clickableChange {
padding: var(--spacing-3xs) var(--spacing-xs) var(--spacing-3xs) 0;
margin-left: -4px;
}
}
}
.clickableChange {
display: flex;
align-items: flex-start;
gap: var(--spacing-2xs);
border-radius: 4px;
padding: var(--spacing-xs) var(--spacing-2xs);
line-height: unset;
}
.clickableChangeActive {
background-color: var(--color-background-medium);
} }
.separator { .separator {
width: 1px; width: 1px;
height: 10px; height: 10px;
background-color: var(--color-foreground-xdark); background-color: var(--color-foreground-xdark);
margin: -5px 23px; margin: 0 0 -5px var(--spacing-xs);
position: relative; position: relative;
z-index: 1; z-index: 1;
} }
.clickableChange {
display: flex;
align-items: center;
gap: 8px;
border-radius: 4px;
}
.clickableChangeActive {
background-color: var(--color-background-medium);
}
.deleted, .deleted,
.added, .added,
.modified { .modified {
position: relative; position: relative;
&::before { &::before {
position: absolute; position: absolute;
bottom: 0; top: 0;
left: 0; left: 50%;
border-bottom-left-radius: 6px; transform: translate(-50%, -50%);
border-top-right-radius: 2px; border-radius: 4px;
color: var(--color-text-xlight); color: var(--color-text-xlight);
font-family: var(--font-family-monospace); font-family: Inter, var(--font-family);
font-size: 8px; font-size: 10px;
font-weight: 700; font-weight: 700;
z-index: 1; z-index: 1;
width: 16px; width: 16px;
@@ -681,51 +726,49 @@ const modifiers = [
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
&[data-node-type='n8n-nodes-base.stickyNote'],
&[data-node-type='n8n-nodes-base.manualTrigger'] {
&::before {
left: auto;
right: 0;
border-top-right-radius: 0;
border-top-left-radius: 2px;
border-bottom-left-radius: 0;
border-bottom-right-radius: 6px;
}
}
} }
.deleted { .deleted {
--canvas-node--background: rgba(234, 31, 48, 0.2); --canvas-node--background: var(--diff-del-faint);
--canvas-node--border-color: var(--color-node-icon-red); --canvas-node--border-color: var(--diff-del);
--color-sticky-background: rgba(234, 31, 48, 0.2); --color-sticky-background: var(--diff-del-faint);
--color-sticky-border: var(--color-node-icon-red); --color-sticky-border: var(--diff-del);
&::before { &::before {
content: 'D'; content: 'D';
background-color: var(--color-node-icon-red); background-color: var(--diff-del);
} }
:global(.canvas-node-handle-main-output > div) { :global(.canvas-node-handle-main-output > div:empty) {
background-color: var(--color-node-icon-red); background-color: var(--diff-del);
} }
:global(.canvas-node-handle-main-input .target) { :global(.canvas-node-handle-main-input .target) {
background-color: var(--color-node-icon-red); background-color: var(--diff-del);
}
/* Ensure disabled nodes still show diff border color */
:global([class*='disabled']) {
--canvas-node--border-color: var(--diff-del) !important;
} }
} }
.added { .added {
--canvas-node--border-color: var(--color-node-icon-green); --canvas-node--border-color: var(--diff-new);
--canvas-node--background: rgba(14, 171, 84, 0.2); --canvas-node--background: var(--diff-new-faint);
--color-sticky-background: rgba(14, 171, 84, 0.2); --color-sticky-background: var(--diff-new-faint);
--color-sticky-border: var(--color-node-icon-green); --color-sticky-border: var(--diff-new);
position: relative; position: relative;
&::before { &::before {
content: 'N'; content: 'N';
background-color: var(--color-node-icon-green); background-color: var(--diff-new);
} }
:global(.canvas-node-handle-main-output > div) { :global(.canvas-node-handle-main-output > div:empty) {
background-color: var(--color-node-icon-green); background-color: var(--diff-new);
} }
:global(.canvas-node-handle-main-input .target) { :global(.canvas-node-handle-main-input .target) {
background-color: var(--color-node-icon-green); background-color: var(--diff-new);
}
/* Ensure disabled nodes still show diff border color */
:global([class*='disabled']) {
--canvas-node--border-color: var(--diff-new) !important;
} }
} }
.equal { .equal {
@@ -741,30 +784,35 @@ const modifiers = [
} }
} }
.modified { .modified {
--canvas-node--border-color: var(--color-node-icon-orange); --canvas-node--border-color: var(--diff-modified);
--canvas-node--background: rgba(255, 150, 90, 0.2); --canvas-node--background: var(--diff-modified-faint);
--color-sticky-background: rgba(255, 150, 90, 0.2); --color-sticky-background: var(--diff-modified-faint);
--color-sticky-border: var(--color-node-icon-orange); --color-sticky-border: var(--diff-modified);
position: relative; position: relative;
&::before { &::before {
content: 'M'; content: 'M';
background-color: var(--color-node-icon-orange); background-color: var(--diff-modified);
} }
:global(.canvas-node-handle-main-output .source) { :global(.canvas-node-handle-main-output > div:empty) {
--color-foreground-xdark: var(--color-node-icon-orange); background-color: var(--diff-modified);
} }
:global(.canvas-node-handle-main-input .target) { :global(.canvas-node-handle-main-input .target) {
background-color: var(--color-node-icon-orange); background-color: var(--diff-modified);
}
/* Ensure disabled nodes still show diff border color */
:global([class*='disabled']) {
--canvas-node--border-color: var(--diff-modified) !important;
} }
} }
.edge-deleted { .edge-deleted {
--canvas-edge-color: var(--color-node-icon-red); --canvas-edge-color: var(--diff-del);
--edge-highlight-color: rgba(234, 31, 48, 0.2); --edge-highlight-color: var(--diff-del-light);
} }
.edge-added { .edge-added {
--canvas-edge-color: var(--color-node-icon-green); --canvas-edge-color: var(--diff-new);
--edge-highlight-color: rgba(14, 171, 84, 0.2); --edge-highlight-color: var(--diff-new-light);
} }
.edge-equal { .edge-equal {
opacity: 0.5; opacity: 0.5;
@@ -772,7 +820,7 @@ const modifiers = [
.noNumberDiff { .noNumberDiff {
min-height: 41px; min-height: 41px;
margin-bottom: 10px !important; margin-bottom: 10px;
:global(.blob-num) { :global(.blob-num) {
display: none; display: none;
} }
@@ -785,8 +833,8 @@ const modifiers = [
width: 16px; width: 16px;
height: 16px; height: 16px;
border-radius: 50%; border-radius: 50%;
background-color: var(--color-background-medium); background-color: var(--color-primary);
color: var(--color-text-dark); color: var(--color-text-xlight);
font-size: 10px; font-size: 10px;
font-weight: bold; font-weight: bold;
line-height: 1; line-height: 1;
@@ -795,6 +843,12 @@ const modifiers = [
.dropdownContent { .dropdownContent {
min-width: 300px; min-width: 300px;
padding: 2px 12px; padding: 2px 12px;
ul {
list-style: none;
margin: 0;
padding: 0;
}
} }
.workflowDiffContent { .workflowDiffContent {
@@ -812,7 +866,6 @@ const modifiers = [
.workflowDiffPanel { .workflowDiffPanel {
flex: 1; flex: 1;
position: relative; position: relative;
border-top: 1px solid #ddd;
} }
.emptyWorkflow { .emptyWorkflow {
@@ -827,15 +880,19 @@ const modifiers = [
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
.navigationButton {
height: 34px;
width: 34px;
}
.backButton {
border: none;
}
} }
.headerLeft { .headerLeft {
display: flex; display: flex;
align-items: center; align-items: center;
} }
.navigationButton {
height: 34px !important;
width: 34px !important;
}
</style> </style>

View File

@@ -173,12 +173,17 @@ export const useWorkflowDiff = (
targetRefs.workflowObjectRef, targetRefs.workflowObjectRef,
); );
const nodesDiff = computed(() => const nodesDiff = computed(() => {
compareWorkflowsNodes( // Don't compute diff until both workflows are loaded to prevent initial flashing
if (!source.value?.workflow?.value || !target.value?.workflow?.value) {
return new Map<string, NodeDiff<INodeUi>>();
}
return compareWorkflowsNodes(
source.value.workflow?.value?.nodes ?? [], source.value.workflow?.value?.nodes ?? [],
target.value.workflow?.value?.nodes ?? [], target.value.workflow?.value?.nodes ?? [],
),
); );
});
type Connection = { type Connection = {
id: string; id: string;
@@ -217,6 +222,11 @@ export const useWorkflowDiff = (
} }
const connectionsDiff = computed(() => { const connectionsDiff = computed(() => {
// Don't compute diff until both workflows are loaded to prevent initial flashing
if (!source.value?.workflow?.value || !target.value?.workflow?.value) {
return new Map<string, { status: NodeDiffStatus; connection: Connection }>();
}
const sourceConnections = mapConnections(source.value?.connections ?? []); const sourceConnections = mapConnections(source.value?.connections ?? []);
const targetConnections = mapConnections(target.value?.connections ?? []); const targetConnections = mapConnections(target.value?.connections ?? []);