Skip to content

Commit 9da4d3d

Browse files
authored
feat(editor): Highlight nodes in canvas on card hover in setup panel (#25862)
1 parent 8c40353 commit 9da4d3d

File tree

5 files changed

+83
-4
lines changed

5 files changed

+83
-4
lines changed

packages/frontend/editor-ui/src/features/setupPanel/components/NodeSetupCard.vue

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup lang="ts">
2-
import { computed, onMounted, ref, watch } from 'vue';
2+
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
33
import { useI18n } from '@n8n/i18n';
44
import { N8nButton, N8nIcon, N8nText, N8nTooltip } from '@n8n/design-system';
55
@@ -8,8 +8,9 @@ import CredentialPicker from '@/features/credentials/components/CredentialPicker
88
import SetupCredentialLabel from './SetupCredentialLabel.vue';
99
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
1010
import { useWorkflowsStore } from '@/app/stores/workflows.store';
11+
import { useSetupPanelStore } from '../setupPanel.store';
1112
12-
import type { NodeSetupState } from '../setupPanel.types';
13+
import type { NodeCredentialRequirement, NodeSetupState } from '../setupPanel.types';
1314
import { useNodeExecution } from '@/app/composables/useNodeExecution';
1415
import { useTelemetry } from '@/app/composables/useTelemetry';
1516
@@ -28,6 +29,7 @@ const i18n = useI18n();
2829
const telemetry = useTelemetry();
2930
const nodeTypesStore = useNodeTypesStore();
3031
const workflowsStore = useWorkflowsStore();
32+
const setupPanelStore = useSetupPanelStore();
3133
3234
const nodeRef = computed(() => props.state.node);
3335
const { isExecuting, buttonLabel, buttonIcon, disabledReason, hasIssues, execute } =
@@ -51,6 +53,27 @@ const tooltipText = computed(() => {
5153
return disabledReason.value;
5254
});
5355
56+
const onCardMouseEnter = () => {
57+
setupPanelStore.setHighlightedNodes([props.state.node.id]);
58+
};
59+
60+
const onSharedNodesHintEnter = (requirement: NodeCredentialRequirement) => {
61+
const ids: string[] = [props.state.node.id];
62+
for (const name of requirement.nodesWithSameCredential) {
63+
const node = workflowsStore.getNodeByName(name);
64+
if (node) ids.push(node.id);
65+
}
66+
setupPanelStore.setHighlightedNodes(ids);
67+
};
68+
69+
const onSharedNodesHintLeave = () => {
70+
setupPanelStore.setHighlightedNodes([props.state.node.id]);
71+
};
72+
73+
const onCardMouseLeave = () => {
74+
setupPanelStore.clearHighlightedNodes();
75+
};
76+
5477
const onHeaderClick = () => {
5578
expanded.value = !expanded.value;
5679
};
@@ -120,6 +143,10 @@ onMounted(() => {
120143
expanded.value = false;
121144
}
122145
});
146+
147+
onBeforeUnmount(() => {
148+
setupPanelStore.clearHighlightedNodes();
149+
});
123150
</script>
124151

125152
<template>
@@ -133,6 +160,8 @@ onMounted(() => {
133160
[$style['no-content']]: !state.credentialRequirements.length,
134161
},
135162
]"
163+
@mouseenter="onCardMouseEnter"
164+
@mouseleave="onCardMouseLeave"
136165
>
137166
<header data-test-id="node-setup-card-header" :class="$style.header" @click="onHeaderClick">
138167
<N8nIcon
@@ -182,6 +211,8 @@ onMounted(() => {
182211
:node-name="state.node.name"
183212
:credential-type="requirement.credentialType"
184213
:nodes-with-same-credential="requirement.nodesWithSameCredential"
214+
@hint-mouse-enter="onSharedNodesHintEnter(requirement)"
215+
@hint-mouse-leave="onSharedNodesHintLeave"
185216
/>
186217
<CredentialPicker
187218
create-button-type="secondary"

packages/frontend/editor-ui/src/features/setupPanel/components/SetupCredentialLabel.vue

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ defineProps<{
88
nodesWithSameCredential: string[];
99
}>();
1010
11+
const emit = defineEmits<{
12+
hintMouseEnter: [];
13+
hintMouseLeave: [];
14+
}>();
15+
1116
const i18n = useI18n();
1217
</script>
1318

@@ -24,7 +29,12 @@ const i18n = useI18n();
2429
<template #content>
2530
{{ nodesWithSameCredential.join(', ') }}
2631
</template>
27-
<span data-test-id="node-setup-card-shared-nodes-hint" :class="$style['shared-nodes-hint']">
32+
<span
33+
data-test-id="node-setup-card-shared-nodes-hint"
34+
:class="$style['shared-nodes-hint']"
35+
@mouseenter="emit('hintMouseEnter')"
36+
@mouseleave="emit('hintMouseLeave')"
37+
>
2838
{{
2939
i18n.baseText('setupPanel.usedInNodes', {
3040
interpolate: {

packages/frontend/editor-ui/src/features/setupPanel/setupPanel.store.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { usePostHog } from '@/app/stores/posthog.store';
22
import { STORES } from '@n8n/stores';
33
import { SETUP_PANEL } from '@/app/constants';
44
import { defineStore } from 'pinia';
5-
import { computed } from 'vue';
5+
import { computed, ref } from 'vue';
66

77
export const useSetupPanelStore = defineStore(STORES.SETUP_PANEL, () => {
88
const posthogStore = usePostHog();
@@ -11,7 +11,23 @@ export const useSetupPanelStore = defineStore(STORES.SETUP_PANEL, () => {
1111
return posthogStore.getVariant(SETUP_PANEL.name) === SETUP_PANEL.variant;
1212
});
1313

14+
const highlightedNodeIds = ref(new Set<string>());
15+
16+
const isHighlightActive = computed(() => highlightedNodeIds.value.size > 0);
17+
18+
function setHighlightedNodes(nodeIds: string[]) {
19+
highlightedNodeIds.value = new Set(nodeIds);
20+
}
21+
22+
function clearHighlightedNodes() {
23+
highlightedNodeIds.value = new Set();
24+
}
25+
1426
return {
1527
isFeatureEnabled,
28+
highlightedNodeIds,
29+
isHighlightActive,
30+
setHighlightedNodes,
31+
clearHighlightedNodes,
1632
};
1733
});

packages/frontend/editor-ui/src/features/workflows/canvas/components/Canvas.vue

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ import { type ContextMenuAction } from '@/features/shared/contextMenu/composable
6666
import { useFocusedNodesStore } from '@/features/ai/assistant/focusedNodes.store';
6767
import { useChatPanelStore } from '@/features/ai/assistant/chatPanel.store';
6868
import { useChatPanelStateStore } from '@/features/ai/assistant/chatPanelState.store';
69+
import { useSetupPanelStore } from '@/features/setupPanel/setupPanel.store';
6970
7071
const $style = useCssModule();
7172
@@ -164,6 +165,7 @@ const experimentalNdvStore = useExperimentalNdvStore();
164165
const focusedNodesStore = useFocusedNodesStore();
165166
const chatPanelStore = useChatPanelStore();
166167
const chatPanelStateStore = useChatPanelStateStore();
168+
const setupPanelStore = useSetupPanelStore();
167169
168170
const isExperimentalNdvActive = computed(() => experimentalNdvStore.isActive(viewport.value.zoom));
169171
@@ -212,6 +214,7 @@ const classes = computed(() => ({
212214
[$style.canvas]: true,
213215
[$style.ready]: !props.loading && isPaneReady.value,
214216
[$style.isExperimentalNdvActive]: isExperimentalNdvActive.value,
217+
spotlightActive: setupPanelStore.isHighlightActive,
215218
}));
216219
217220
/**
@@ -1054,6 +1057,7 @@ defineExpose({
10541057
:event-bus="eventBus"
10551058
:hovered="nodesHoveredById[nodeProps.id]"
10561059
:nearby-hovered="nodeProps.id === hoveredTriggerNode.id.value"
1060+
:highlighted="setupPanelStore.highlightedNodeIds.has(nodeProps.id)"
10571061
@delete="onDeleteNode"
10581062
@run="onRunNode"
10591063
@select="onSelectNode"
@@ -1169,4 +1173,20 @@ defineExpose({
11691173
.minimap-leave-to {
11701174
opacity: 0;
11711175
}
1176+
1177+
.spotlightActive {
1178+
:deep(.vue-flow__edges) {
1179+
opacity: 0.2;
1180+
transition: opacity 0.15s ease;
1181+
}
1182+
1183+
:deep(.vue-flow__node) {
1184+
opacity: 0.2;
1185+
transition: opacity 0.15s ease;
1186+
}
1187+
1188+
:deep(.vue-flow__node:has(.highlighted)) {
1189+
opacity: 1;
1190+
}
1191+
}
11721192
</style>

packages/frontend/editor-ui/src/features/workflows/canvas/components/elements/nodes/CanvasNode.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ type Props = NodeProps<CanvasNodeData> & {
4141
eventBus?: EventBus<CanvasEventBusEvents>;
4242
hovered?: boolean;
4343
nearbyHovered?: boolean;
44+
highlighted?: boolean;
4445
};
4546
4647
const slots = defineSlots<{
@@ -102,6 +103,7 @@ const classes = computed(() => ({
102103
[style.canvasNode]: true,
103104
[style.showToolbar]: showToolbar.value,
104105
hovered: props.hovered,
106+
highlighted: props.highlighted,
105107
selected: props.selected,
106108
waiting: props.data.execution.waiting || props.data.execution.status === 'waiting',
107109
running: props.data.execution.running || props.data.execution.waitingForNext,

0 commit comments

Comments
 (0)