Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue';
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { useI18n } from '@n8n/i18n';
import { N8nButton, N8nIcon, N8nText, N8nTooltip } from '@n8n/design-system';

Expand All @@ -8,8 +8,9 @@ import CredentialPicker from '@/features/credentials/components/CredentialPicker
import SetupCredentialLabel from './SetupCredentialLabel.vue';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { useSetupPanelStore } from '../setupPanel.store';

import type { NodeSetupState } from '../setupPanel.types';
import type { NodeCredentialRequirement, NodeSetupState } from '../setupPanel.types';
import { useNodeExecution } from '@/app/composables/useNodeExecution';
import { useTelemetry } from '@/app/composables/useTelemetry';

Expand All @@ -32,6 +33,7 @@ const i18n = useI18n();
const telemetry = useTelemetry();
const nodeTypesStore = useNodeTypesStore();
const workflowsStore = useWorkflowsStore();
const setupPanelStore = useSetupPanelStore();

const nodeRef = computed(() => props.state.node);
const { isExecuting, buttonLabel, buttonIcon, disabledReason, hasIssues, execute } =
Expand All @@ -55,6 +57,27 @@ const tooltipText = computed(() => {
return disabledReason.value;
});

const onCardMouseEnter = () => {
setupPanelStore.setHighlightedNodes([props.state.node.id]);
};

const onSharedNodesHintEnter = (requirement: NodeCredentialRequirement) => {
const ids: string[] = [props.state.node.id];
for (const name of requirement.nodesWithSameCredential) {
const node = workflowsStore.getNodeByName(name);
if (node) ids.push(node.id);
}
setupPanelStore.setHighlightedNodes(ids);
};

const onSharedNodesHintLeave = () => {
setupPanelStore.setHighlightedNodes([props.state.node.id]);
};

const onCardMouseLeave = () => {
setupPanelStore.clearHighlightedNodes();
};

const onHeaderClick = () => {
expanded.value = !expanded.value;
};
Expand Down Expand Up @@ -124,6 +147,10 @@ onMounted(() => {
expanded.value = false;
}
});

onBeforeUnmount(() => {
setupPanelStore.clearHighlightedNodes();
});
</script>

<template>
Expand All @@ -137,6 +164,8 @@ onMounted(() => {
[$style['no-content']]: !state.credentialRequirements.length,
},
]"
@mouseenter="onCardMouseEnter"
@mouseleave="onCardMouseLeave"
>
<header data-test-id="node-setup-card-header" :class="$style.header" @click="onHeaderClick">
<N8nIcon
Expand Down Expand Up @@ -193,6 +222,8 @@ onMounted(() => {
:node-name="state.node.name"
:credential-type="requirement.credentialType"
:nodes-with-same-credential="requirement.nodesWithSameCredential"
@hint-mouse-enter="onSharedNodesHintEnter(requirement)"
@hint-mouse-leave="onSharedNodesHintLeave"
/>
<CredentialPicker
create-button-type="secondary"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ defineProps<{
nodesWithSameCredential: string[];
}>();

const emit = defineEmits<{
hintMouseEnter: [];
hintMouseLeave: [];
}>();

const i18n = useI18n();
</script>

Expand All @@ -24,7 +29,12 @@ const i18n = useI18n();
<template #content>
{{ nodesWithSameCredential.join(', ') }}
</template>
<span data-test-id="node-setup-card-shared-nodes-hint" :class="$style['shared-nodes-hint']">
<span
data-test-id="node-setup-card-shared-nodes-hint"
:class="$style['shared-nodes-hint']"
@mouseenter="emit('hintMouseEnter')"
@mouseleave="emit('hintMouseLeave')"
>
{{
i18n.baseText('setupPanel.usedInNodes', {
interpolate: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { usePostHog } from '@/app/stores/posthog.store';
import { STORES } from '@n8n/stores';
import { SETUP_PANEL } from '@/app/constants';
import { defineStore } from 'pinia';
import { computed } from 'vue';
import { computed, ref } from 'vue';

export const useSetupPanelStore = defineStore(STORES.SETUP_PANEL, () => {
const posthogStore = usePostHog();
Expand All @@ -11,7 +11,43 @@ export const useSetupPanelStore = defineStore(STORES.SETUP_PANEL, () => {
return posthogStore.getVariant(SETUP_PANEL.name) === SETUP_PANEL.variant;
});

const credentialsPendingTest = ref(new Set<string>());

const highlightedNodeIds = ref(new Set<string>());

const isHighlightActive = computed(() => highlightedNodeIds.value.size > 0);

function setHighlightedNodes(nodeIds: string[]) {
highlightedNodeIds.value = new Set(nodeIds);
}

function clearHighlightedNodes() {
highlightedNodeIds.value = new Set();
}

function addPendingTest(credentialId: string) {
if (!isFeatureEnabled.value) return;
credentialsPendingTest.value = new Set([...credentialsPendingTest.value, credentialId]);
}

function removePendingTest(credentialId: string) {
const next = new Set(credentialsPendingTest.value);
next.delete(credentialId);
credentialsPendingTest.value = next;
}

function isCredentialPendingTest(credentialId: string): boolean {
return credentialsPendingTest.value.has(credentialId);
}

return {
isFeatureEnabled,
addPendingTest,
removePendingTest,
isCredentialPendingTest,
highlightedNodeIds,
isHighlightActive,
setHighlightedNodes,
clearHighlightedNodes,
};
});
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import { type ContextMenuAction } from '@/features/shared/contextMenu/composable
import { useFocusedNodesStore } from '@/features/ai/assistant/focusedNodes.store';
import { useChatPanelStore } from '@/features/ai/assistant/chatPanel.store';
import { useChatPanelStateStore } from '@/features/ai/assistant/chatPanelState.store';
import { useSetupPanelStore } from '@/features/setupPanel/setupPanel.store';

const $style = useCssModule();

Expand Down Expand Up @@ -164,6 +165,7 @@ const experimentalNdvStore = useExperimentalNdvStore();
const focusedNodesStore = useFocusedNodesStore();
const chatPanelStore = useChatPanelStore();
const chatPanelStateStore = useChatPanelStateStore();
const setupPanelStore = useSetupPanelStore();

const isExperimentalNdvActive = computed(() => experimentalNdvStore.isActive(viewport.value.zoom));

Expand Down Expand Up @@ -212,6 +214,7 @@ const classes = computed(() => ({
[$style.canvas]: true,
[$style.ready]: !props.loading && isPaneReady.value,
[$style.isExperimentalNdvActive]: isExperimentalNdvActive.value,
spotlightActive: setupPanelStore.isHighlightActive,
}));

/**
Expand Down Expand Up @@ -1054,6 +1057,7 @@ defineExpose({
:event-bus="eventBus"
:hovered="nodesHoveredById[nodeProps.id]"
:nearby-hovered="nodeProps.id === hoveredTriggerNode.id.value"
:highlighted="setupPanelStore.highlightedNodeIds.has(nodeProps.id)"
@delete="onDeleteNode"
@run="onRunNode"
@select="onSelectNode"
Expand Down Expand Up @@ -1169,4 +1173,20 @@ defineExpose({
.minimap-leave-to {
opacity: 0;
}

.spotlightActive {
:deep(.vue-flow__edges) {
opacity: 0.2;
transition: opacity 0.15s ease;
}

:deep(.vue-flow__node) {
opacity: 0.2;
transition: opacity 0.15s ease;
}

:deep(.vue-flow__node:has(.highlighted)) {
opacity: 1;
}
}
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ type Props = NodeProps<CanvasNodeData> & {
eventBus?: EventBus<CanvasEventBusEvents>;
hovered?: boolean;
nearbyHovered?: boolean;
highlighted?: boolean;
};

const slots = defineSlots<{
Expand Down Expand Up @@ -102,6 +103,7 @@ const classes = computed(() => ({
[style.canvasNode]: true,
[style.showToolbar]: showToolbar.value,
hovered: props.hovered,
highlighted: props.highlighted,
selected: props.selected,
waiting: props.data.execution.waiting || props.data.execution.status === 'waiting',
running: props.data.execution.running || props.data.execution.waitingForNext,
Expand Down