From bc7664397607972dae72d97b2cacf592d3c93f83 Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Thu, 7 Aug 2025 16:50:42 +0200 Subject: [PATCH] fix(editor): Enhance changes dropdown in WorkflowDiffModal (#18033) --- .../frontend/@n8n/i18n/src/locales/en.json | 12 +- ...odal.test.ts => WorkflowDiffModal.test.ts} | 88 ++++++ .../workflow-diff/WorkflowDiffModal.vue | 269 +++++++++++------- 3 files changed, 258 insertions(+), 111 deletions(-) rename packages/frontend/editor-ui/src/features/workflow-diff/{WorkflowDIffModal.test.ts => WorkflowDiffModal.test.ts} (78%) diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json index a6d6d67257b..1ccb0cedb68 100644 --- a/packages/frontend/@n8n/i18n/src/locales/en.json +++ b/packages/frontend/@n8n/i18n/src/locales/en.json @@ -3382,5 +3382,15 @@ "whatsNew.versionsBehind": "{count} version behind | {count} versions behind", "whatsNew.update": "Update", "whatsNew.updateAvailable": "You're currently on version {currentVersion}. Update to {latestVersion} to get {count} versions worth of new features, improvements, and fixes. See what changed", - "whatsNew.updateAvailable.changelogLink": "in the full changelog" + "whatsNew.updateAvailable.changelogLink": "in the full changelog", + "workflowDiff.changes": "Changes", + "workflowDiff.nodes": "Nodes", + "workflowDiff.connectors": "Connectors", + "workflowDiff.settings": "Settings", + "workflowDiff.local": "Local", + "workflowDiff.remote": "Remote ({branchName})", + "workflowDiff.noChanges": "No changes", + "workflowDiff.deletedWorkflow": "Deleted workflow", + "workflowDiff.deletedWorkflow.database": "The workflow was deleted on the database", + "workflowDiff.deletedWorkflow.remote": "The workflow was deleted on remote" } diff --git a/packages/frontend/editor-ui/src/features/workflow-diff/WorkflowDIffModal.test.ts b/packages/frontend/editor-ui/src/features/workflow-diff/WorkflowDiffModal.test.ts similarity index 78% rename from packages/frontend/editor-ui/src/features/workflow-diff/WorkflowDIffModal.test.ts rename to packages/frontend/editor-ui/src/features/workflow-diff/WorkflowDiffModal.test.ts index c98f810f7c5..fdcc6b981d1 100644 --- a/packages/frontend/editor-ui/src/features/workflow-diff/WorkflowDIffModal.test.ts +++ b/packages/frontend/editor-ui/src/features/workflow-diff/WorkflowDiffModal.test.ts @@ -302,4 +302,92 @@ describe('WorkflowDiffModal', () => { expect(pullComponent.container.querySelector('.header')).toBeInTheDocument(); expect(pushComponent.container.querySelector('.header')).toBeInTheDocument(); }); + + it('should show empty state when no changes exist in tabs', async () => { + const { getByText } = renderModal({ + pinia: createTestingPinia(), + props: { + data: { + eventBus, + workflowId: 'test-workflow-id', + direction: 'push', + }, + }, + }); + + // Open changes dropdown + const changesButton = getByText('Changes'); + await userEvent.click(changesButton); + + // Wait for dropdown to open and check tabs + await waitFor(() => { + expect(getByText('Nodes')).toBeInTheDocument(); + }); + + // Click on Nodes tab to make it active + await userEvent.click(getByText('Nodes')); + + // Should show "No changes" when there are no node changes + await waitFor(() => { + expect(getByText('No changes')).toBeInTheDocument(); + }); + }); + + it('should show empty state for connectors tab when no connector changes', async () => { + const { getByText } = renderModal({ + pinia: createTestingPinia(), + props: { + data: { + eventBus, + workflowId: 'test-workflow-id', + direction: 'push', + }, + }, + }); + + // Open changes dropdown + const changesButton = getByText('Changes'); + await userEvent.click(changesButton); + + await waitFor(() => { + expect(getByText('Connectors')).toBeInTheDocument(); + }); + + // Click on Connectors tab + await userEvent.click(getByText('Connectors')); + + // Should show "No changes" when there are no connector changes + await waitFor(() => { + expect(getByText('No changes')).toBeInTheDocument(); + }); + }); + + it('should show empty state for settings tab when no settings changes', async () => { + const { getByText } = renderModal({ + pinia: createTestingPinia(), + props: { + data: { + eventBus, + workflowId: 'test-workflow-id', + direction: 'push', + }, + }, + }); + + // Open changes dropdown + const changesButton = getByText('Changes'); + await userEvent.click(changesButton); + + await waitFor(() => { + expect(getByText('Settings')).toBeInTheDocument(); + }); + + // Click on Settings tab + await userEvent.click(getByText('Settings')); + + // Should show "No changes" when there are no settings changes + await waitFor(() => { + expect(getByText('No changes')).toBeInTheDocument(); + }); + }); }); diff --git a/packages/frontend/editor-ui/src/features/workflow-diff/WorkflowDiffModal.vue b/packages/frontend/editor-ui/src/features/workflow-diff/WorkflowDiffModal.vue index 5d3b2898832..ad13ac8cc5e 100644 --- a/packages/frontend/editor-ui/src/features/workflow-diff/WorkflowDiffModal.vue +++ b/packages/frontend/editor-ui/src/features/workflow-diff/WorkflowDiffModal.vue @@ -187,24 +187,24 @@ const activeTab = ref<'nodes' | 'connectors' | 'settings'>(); const tabs = computed(() => [ { value: 'nodes' as const, - label: 'Nodes', - disabled: nodeChanges.value.length === 0, + label: i18n.baseText('workflowDiff.nodes'), + disabled: false, data: { count: nodeChanges.value.length, }, }, { value: 'connectors' as const, - label: 'Connectors', - disabled: connectionsDiff.value.size === 0, + label: i18n.baseText('workflowDiff.connectors'), + disabled: false, data: { count: connectionsDiff.value.size, }, }, { value: 'settings' as const, - label: 'Settings', - disabled: settingsDiff.value.length === 0, + label: i18n.baseText('workflowDiff.settings'), + disabled: false, data: { count: settingsDiff.value.length, }, @@ -220,8 +220,7 @@ function setActiveTab(active: boolean) { telemetry.track('User clicked workflow diff changes button', { workflow_id: props.data.workflowId, }); - const value = tabs.value.find((tab) => !tab.disabled)?.value ?? 'nodes'; - activeTab.value = value; + activeTab.value = 'nodes'; } function trackTabChange(value: 'nodes' | 'connectors' | 'settings') { @@ -367,7 +366,7 @@ const modifiers = [
{{ changesCount }}
- Changes + {{ i18n.baseText('workflowDiff.changes') }} @@ -538,8 +561,10 @@ const modifiers = [ {{ targetWorkFlow.state.value.remote - ? `Remote (${sourceControlStore.preferences.branchName})` - : 'Local' + ? i18n.baseText('workflowDiff.remote', { + interpolate: { branchName: sourceControlStore.preferences.branchName }, + }) + : i18n.baseText('workflowDiff.local') }} @@ -651,8 +677,8 @@ const modifiers = [ gap: var(--spacing-2xs); padding: 10px 0 var(--spacing-3xs) var(--spacing-2xs); - ul { - margin-top: -3px; + > div { + min-width: 0; } .clickableChange { @@ -660,6 +686,12 @@ const modifiers = [ margin-left: -4px; } } + + .changesNested { + margin-top: -3px; + width: 100%; + min-width: 0; + } } .clickableChange { @@ -668,12 +700,18 @@ const modifiers = [ gap: var(--spacing-2xs); border-radius: 4px; padding: var(--spacing-xs) var(--spacing-2xs); + margin-right: var(--spacing-xs); line-height: unset; min-width: 0; + transition: background-color 0.2s ease; + + &:hover { + background-color: var(--color-background-xlight); + } } .clickableChangeActive { - background-color: var(--color-background-medium); + background-color: var(--color-background-xlight); } .nodeName { @@ -806,14 +844,6 @@ const modifiers = [ opacity: 0.5; } -.noNumberDiff { - min-height: 41px; - margin-bottom: 10px; - :global(.blob-num) { - display: none; - } -} - .circleBadge { display: inline-flex; align-items: center; @@ -829,13 +859,25 @@ const modifiers = [ } .dropdownContent { - min-width: 300px; - padding: 2px 12px; + width: 320px; + padding: 2px 0 2px 12px; ul { list-style: none; margin: 0; padding: 0; + max-height: 400px; + overflow-x: hidden; + overflow-y: auto; + } + + .noNumberDiff { + min-height: 41px; + margin: 0 12px 10px 0; + overflow: hidden; + :global(.blob-num) { + display: none; + } } } @@ -883,4 +925,11 @@ const modifiers = [ display: flex; align-items: center; } + +.emptyState { + display: flex; + justify-content: center; + align-items: center; + padding: var(--spacing-m) var(--spacing-xs); +}