mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
fix(editor): Ai 668 UI changes to mock nodes modal (no-changelog) (#13899)
Co-authored-by: Oleg Ivaniv <me@olegivaniv.com>
This commit is contained in:
committed by
GitHub
parent
c91688d494
commit
4a1e5798ff
@@ -1,15 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import Canvas from '@/components/canvas/Canvas.vue';
|
||||||
|
import CanvasNode from '@/components/canvas/elements/nodes/CanvasNode.vue';
|
||||||
import { useCanvasMapping } from '@/composables/useCanvasMapping';
|
import { useCanvasMapping } from '@/composables/useCanvasMapping';
|
||||||
import { useCanvasOperations } from '@/composables/useCanvasOperations';
|
import { useCanvasOperations } from '@/composables/useCanvasOperations';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import { useTelemetry } from '@/composables/useTelemetry';
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import type { CanvasConnectionPort, CanvasEventBusEvents, CanvasNodeData } from '@/types';
|
import type { CanvasConnectionPort, CanvasNodeData } from '@/types';
|
||||||
|
import { N8nButton, N8nHeading, N8nSpinner, N8nText, N8nTooltip } from '@n8n/design-system';
|
||||||
import { useVueFlow } from '@vue-flow/core';
|
import { useVueFlow } from '@vue-flow/core';
|
||||||
import { N8nTooltip } from '@n8n/design-system';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
import { createEventBus } from '@n8n/utils/event-bus';
|
|
||||||
import { computed, onMounted, ref, useCssModule } from 'vue';
|
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
const workflowsStore = useWorkflowsStore();
|
const workflowsStore = useWorkflowsStore();
|
||||||
@@ -21,18 +22,12 @@ const telemetry = useTelemetry();
|
|||||||
|
|
||||||
const { resetWorkspace, initializeWorkspace } = useCanvasOperations({ router });
|
const { resetWorkspace, initializeWorkspace } = useCanvasOperations({ router });
|
||||||
|
|
||||||
const eventBus = createEventBus<CanvasEventBusEvents>();
|
|
||||||
const style = useCssModule();
|
|
||||||
const uuid = crypto.randomUUID();
|
const uuid = crypto.randomUUID();
|
||||||
const props = defineProps<{
|
|
||||||
modelValue: Array<{ name: string; id: string }>;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
type PinnedNode = { name: string; id: string };
|
||||||
'update:modelValue': [value: Array<{ name: string; id: string }>];
|
const model = defineModel<PinnedNode[]>({ required: true });
|
||||||
}>();
|
|
||||||
|
|
||||||
const isLoading = ref(true);
|
const isLoading = ref(false);
|
||||||
|
|
||||||
const workflowId = computed(() => route.params.name as string);
|
const workflowId = computed(() => route.params.name as string);
|
||||||
const testId = computed(() => route.params.testId as string);
|
const testId = computed(() => route.params.testId as string);
|
||||||
@@ -40,10 +35,10 @@ const workflow = computed(() => workflowsStore.getWorkflowById(workflowId.value)
|
|||||||
const workflowObject = computed(() => workflowsStore.getCurrentWorkflow(true));
|
const workflowObject = computed(() => workflowsStore.getCurrentWorkflow(true));
|
||||||
const canvasId = computed(() => `${uuid}-${testId.value}`);
|
const canvasId = computed(() => `${uuid}-${testId.value}`);
|
||||||
|
|
||||||
const { onNodesInitialized, fitView, zoomTo } = useVueFlow({ id: canvasId.value });
|
const { onNodesInitialized, fitView, zoomTo, onNodeClick, viewport } = useVueFlow({
|
||||||
const nodes = computed(() => {
|
id: canvasId.value,
|
||||||
return workflow.value.nodes ?? [];
|
|
||||||
});
|
});
|
||||||
|
const nodes = computed(() => workflow.value.nodes ?? []);
|
||||||
const connections = computed(() => workflow.value.connections);
|
const connections = computed(() => workflow.value.connections);
|
||||||
|
|
||||||
const { nodes: mappedNodes, connections: mappedConnections } = useCanvasMapping({
|
const { nodes: mappedNodes, connections: mappedConnections } = useCanvasMapping({
|
||||||
@@ -51,144 +46,164 @@ const { nodes: mappedNodes, connections: mappedConnections } = useCanvasMapping(
|
|||||||
connections,
|
connections,
|
||||||
workflowObject,
|
workflowObject,
|
||||||
});
|
});
|
||||||
|
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
|
isLoading.value = true;
|
||||||
workflowsStore.resetState();
|
workflowsStore.resetState();
|
||||||
resetWorkspace();
|
resetWorkspace();
|
||||||
const loadingPromise = Promise.all([
|
await Promise.all([
|
||||||
nodeTypesStore.getNodeTypes(),
|
nodeTypesStore.getNodeTypes(),
|
||||||
workflowsStore.fetchWorkflow(workflowId.value),
|
workflowsStore.fetchWorkflow(workflowId.value),
|
||||||
]);
|
]);
|
||||||
await loadingPromise;
|
|
||||||
|
// remove editor pinned data
|
||||||
|
workflow.value.pinData = {};
|
||||||
initializeWorkspace(workflow.value);
|
initializeWorkspace(workflow.value);
|
||||||
disableAllNodes();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNodeNameById(id: string) {
|
function getNodeNameById(id: string) {
|
||||||
return mappedNodes.value.find((node) => node.id === id)?.data?.name;
|
return mappedNodes.value.find((node) => node.id === id)?.data?.name;
|
||||||
}
|
}
|
||||||
function updateNodeClasses(nodeIds: string[], isPinned: boolean) {
|
|
||||||
eventBus.emit('nodes:action', {
|
|
||||||
ids: nodeIds,
|
|
||||||
action: 'update:node:class',
|
|
||||||
payload: {
|
|
||||||
className: style.pinnedNode,
|
|
||||||
add: isPinned,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
eventBus.emit('nodes:action', {
|
|
||||||
ids: nodeIds,
|
|
||||||
action: 'update:node:class',
|
|
||||||
payload: {
|
|
||||||
className: style.notPinnedNode,
|
|
||||||
add: !isPinned,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function disableAllNodes() {
|
|
||||||
const ids = mappedNodes.value.map((node) => node.id);
|
|
||||||
updateNodeClasses(ids, false);
|
|
||||||
|
|
||||||
const pinnedNodes = props.modelValue.map((node) => node.id).filter((id) => id !== null);
|
function isMocked(data: CanvasNodeData) {
|
||||||
|
return model.value.some((node) => node.id === data.id);
|
||||||
if (pinnedNodes.length > 0) {
|
|
||||||
updateNodeClasses(pinnedNodes, true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
function onPinButtonClick(data: CanvasNodeData) {
|
|
||||||
|
function canBeMocked(outputs: CanvasConnectionPort[], inputs: CanvasConnectionPort[]) {
|
||||||
|
return outputs.length === 1 && inputs.length >= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNodeClick(data: CanvasNodeData) {
|
||||||
const nodeName = getNodeNameById(data.id);
|
const nodeName = getNodeNameById(data.id);
|
||||||
if (!nodeName) return;
|
if (!nodeName || !canBeMocked(data.outputs, data.inputs)) return;
|
||||||
|
|
||||||
const isPinned = props.modelValue.some((node) => node.id === data.id);
|
const mocked = isMocked(data);
|
||||||
const updatedNodes = isPinned
|
|
||||||
? props.modelValue.filter((node) => node.id !== data.id)
|
|
||||||
: [...props.modelValue, { name: nodeName, id: data.id }];
|
|
||||||
|
|
||||||
emit('update:modelValue', updatedNodes);
|
model.value = mocked
|
||||||
updateNodeClasses([data.id], !isPinned);
|
? model.value.filter((node) => node.id !== data.id)
|
||||||
|
: model.value.concat({ name: nodeName, id: data.id });
|
||||||
|
|
||||||
if (!isPinned) {
|
if (!mocked) {
|
||||||
telemetry.track('User selected node to be mocked', {
|
telemetry.track('User selected node to be mocked', {
|
||||||
node_id: data.id,
|
node_id: data.id,
|
||||||
test_id: testId.value,
|
test_id: testId.value,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function isPinButtonVisible(outputs: CanvasConnectionPort[], inputs: CanvasConnectionPort[]) {
|
|
||||||
return outputs.length === 1 && inputs.length >= 1;
|
function tooltipContent(data: CanvasNodeData) {
|
||||||
|
if (nodeTypesStore.isTriggerNode(data.type)) {
|
||||||
|
return locale.baseText('testDefinition.edit.nodesPinning.triggerTooltip');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!canBeMocked(data.outputs, data.inputs)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMocked(data)) {
|
||||||
|
return locale.baseText('testDefinition.edit.nodesPinning.pinButtonTooltip.pinned');
|
||||||
|
} else {
|
||||||
|
return locale.baseText('testDefinition.edit.nodesPinning.pinButtonTooltip');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isPinned = (data: CanvasNodeData) => props.modelValue.some((node) => node.id === data.id);
|
function tooltipOffset(data: CanvasNodeData) {
|
||||||
|
if (nodeTypesStore.isTriggerNode(data.type)) return;
|
||||||
|
|
||||||
|
return 45 * viewport.value.zoom;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tooltipProps(data: CanvasNodeData) {
|
||||||
|
const content = tooltipContent(data);
|
||||||
|
return {
|
||||||
|
disabled: !content,
|
||||||
|
content,
|
||||||
|
offset: tooltipOffset(data),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onNodeClick(({ node }) => handleNodeClick(node.data));
|
||||||
|
|
||||||
onNodesInitialized(async () => {
|
onNodesInitialized(async () => {
|
||||||
await fitView();
|
await fitView();
|
||||||
|
await zoomTo(0.7);
|
||||||
|
// Wait for the zoom to be applied and the canvas edges to recompute
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 400));
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
await zoomTo(0.7, { duration: 400 });
|
|
||||||
});
|
});
|
||||||
onMounted(loadData);
|
onMounted(loadData);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="mappedNodes.length === 0" :class="$style.noNodes">
|
<div v-if="mappedNodes.length === 0" :class="$style.noNodes">
|
||||||
<N8nHeading size="large" :bold="true" :class="$style.noNodesTitle">{{
|
<N8nHeading size="large" :bold="true" :class="$style.noNodesTitle">
|
||||||
locale.baseText('testDefinition.edit.pinNodes.noNodes.title')
|
{{ locale.baseText('testDefinition.edit.pinNodes.noNodes.title') }}
|
||||||
}}</N8nHeading>
|
</N8nHeading>
|
||||||
<N8nText>{{ locale.baseText('testDefinition.edit.pinNodes.noNodes.description') }}</N8nText>
|
<N8nText>{{ locale.baseText('testDefinition.edit.pinNodes.noNodes.description') }}</N8nText>
|
||||||
</div>
|
</div>
|
||||||
<div v-else :class="$style.container">
|
<div v-else :class="$style.container">
|
||||||
<N8nSpinner v-if="isLoading" size="xlarge" type="dots" :class="$style.spinner" />
|
<N8nSpinner v-if="isLoading" size="large" type="dots" :class="$style.spinner" />
|
||||||
<Canvas
|
<Canvas
|
||||||
:id="canvasId"
|
:id="canvasId"
|
||||||
:loading="isLoading"
|
:loading="isLoading"
|
||||||
:class="{ [$style.canvas]: true }"
|
|
||||||
:nodes="mappedNodes"
|
:nodes="mappedNodes"
|
||||||
:connections="mappedConnections"
|
:connections="mappedConnections"
|
||||||
:show-bug-reporting-button="false"
|
:show-bug-reporting-button="false"
|
||||||
:read-only="true"
|
:read-only="true"
|
||||||
:event-bus="eventBus"
|
|
||||||
>
|
>
|
||||||
<template #nodeToolbar="{ data, outputs, inputs }">
|
<template #node="{ nodeProps }">
|
||||||
<div
|
<N8nTooltip placement="top" v-bind="tooltipProps(nodeProps.data)">
|
||||||
v-if="isPinButtonVisible(outputs, inputs)"
|
<CanvasNode
|
||||||
:class="{
|
v-bind="nodeProps"
|
||||||
[$style.pinButtonContainer]: true,
|
:class="{
|
||||||
[$style.pinButtonContainerPinned]: isPinned(data),
|
[$style.isTrigger]: nodeTypesStore.isTriggerNode(nodeProps.data.type),
|
||||||
}"
|
[$style.mockNode]: true,
|
||||||
>
|
}"
|
||||||
<N8nTooltip placement="left">
|
>
|
||||||
<template #content>
|
<template #toolbar="{ data, outputs, inputs }">
|
||||||
{{ locale.baseText('testDefinition.edit.nodesPinning.pinButtonTooltip') }}
|
<div
|
||||||
|
v-if="canBeMocked(outputs, inputs)"
|
||||||
|
:class="{
|
||||||
|
[$style.pinButtonContainer]: true,
|
||||||
|
[$style.pinButtonContainerPinned]: isMocked(data),
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<N8nButton
|
||||||
|
icon="thumbtack"
|
||||||
|
block
|
||||||
|
type="secondary"
|
||||||
|
:class="{ [$style.customSecondary]: isMocked(data) }"
|
||||||
|
data-test-id="node-pin-button"
|
||||||
|
>
|
||||||
|
<template v-if="isMocked(data)">
|
||||||
|
{{ locale.baseText('contextMenu.unpin') }}
|
||||||
|
</template>
|
||||||
|
<template v-else> {{ locale.baseText('contextMenu.pin') }}</template>
|
||||||
|
</N8nButton>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<N8nButton
|
</CanvasNode>
|
||||||
v-if="isPinned(data)"
|
</N8nTooltip>
|
||||||
icon="thumbtack"
|
|
||||||
block
|
|
||||||
type="secondary"
|
|
||||||
:class="$style.customSecondary"
|
|
||||||
data-test-id="node-pin-button"
|
|
||||||
@click="onPinButtonClick(data)"
|
|
||||||
>
|
|
||||||
Un Mock
|
|
||||||
</N8nButton>
|
|
||||||
<N8nButton
|
|
||||||
v-else
|
|
||||||
icon="thumbtack"
|
|
||||||
block
|
|
||||||
type="secondary"
|
|
||||||
data-test-id="node-pin-button"
|
|
||||||
@click="onPinButtonClick(data)"
|
|
||||||
>
|
|
||||||
Mock
|
|
||||||
</N8nButton>
|
|
||||||
</N8nTooltip>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</Canvas>
|
</Canvas>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
.mockNode {
|
||||||
|
// remove selection outline
|
||||||
|
--color-canvas-selected-transparent: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.isTrigger {
|
||||||
|
--canvas-node--border-color: var(--color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
width: 100vw;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
border: 1px solid var(--color-foreground-light);
|
||||||
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
.pinButtonContainer {
|
.pinButtonContainer {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -202,16 +217,16 @@ onMounted(loadData);
|
|||||||
transform: translateX(50%);
|
transform: translateX(50%);
|
||||||
|
|
||||||
&.pinButtonContainerPinned {
|
&.pinButtonContainerPinned {
|
||||||
background-color: hsla(247, 49%, 55%, 1);
|
background-color: var(--color-secondary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.customSecondary {
|
.customSecondary {
|
||||||
--button-background-color: hsla(247, 49%, 55%, 1);
|
--button-background-color: var(--color-secondary);
|
||||||
--button-font-color: var(--color-button-primary-font);
|
--button-font-color: var(--color-button-primary-font);
|
||||||
--button-border-color: hsla(247, 49%, 55%, 1);
|
--button-border-color: var(--color-secondary);
|
||||||
|
|
||||||
--button-hover-background-color: hsla(247, 49%, 55%, 1);
|
--button-hover-background-color: var(--color-secondary);
|
||||||
--button-hover-border-color: var(--color-button-primary-font);
|
--button-hover-border-color: var(--color-button-primary-font);
|
||||||
--button-hover-font-color: var(--color-button-primary-font);
|
--button-hover-font-color: var(--color-button-primary-font);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,16 @@ import type { TestMetricRecord } from '@/api/testDefinition.ee';
|
|||||||
import BlockArrow from '@/components/TestDefinition/EditDefinition/BlockArrow.vue';
|
import BlockArrow from '@/components/TestDefinition/EditDefinition/BlockArrow.vue';
|
||||||
import EvaluationStep from '@/components/TestDefinition/EditDefinition/EvaluationStep.vue';
|
import EvaluationStep from '@/components/TestDefinition/EditDefinition/EvaluationStep.vue';
|
||||||
import MetricsInput from '@/components/TestDefinition/EditDefinition/MetricsInput.vue';
|
import MetricsInput from '@/components/TestDefinition/EditDefinition/MetricsInput.vue';
|
||||||
|
import NodesPinning from '@/components/TestDefinition/EditDefinition/NodesPinning.vue';
|
||||||
import WorkflowSelector from '@/components/TestDefinition/EditDefinition/WorkflowSelector.vue';
|
import WorkflowSelector from '@/components/TestDefinition/EditDefinition/WorkflowSelector.vue';
|
||||||
import type { EditableFormState, EvaluationFormState } from '@/components/TestDefinition/types';
|
import type { EditableFormState, EvaluationFormState } from '@/components/TestDefinition/types';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import { useMessage } from '@/composables/useMessage';
|
import { useMessage } from '@/composables/useMessage';
|
||||||
import { NODE_PINNING_MODAL_KEY } from '@/constants';
|
import { NODE_PINNING_MODAL_KEY } from '@/constants';
|
||||||
import type { ITag, ModalState } from '@/Interface';
|
import type { ITag } from '@/Interface';
|
||||||
import { N8nButton, N8nTag, N8nText } from '@n8n/design-system';
|
import { N8nButton, N8nHeading, N8nTag, N8nText } from '@n8n/design-system';
|
||||||
import type { IPinData } from 'n8n-workflow';
|
import type { IPinData } from 'n8n-workflow';
|
||||||
import { computed, ref } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
tagsById: Record<string, ITag>;
|
tagsById: Record<string, ITag>;
|
||||||
@@ -68,8 +69,6 @@ const mockedNodes = defineModel<EvaluationFormState['mockedNodes']>('mockedNodes
|
|||||||
required: true,
|
required: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const nodePinningModal = ref<ModalState | null>(null);
|
|
||||||
|
|
||||||
const selectedTag = computed(() => props.tagsById[tags.value.value[0]] ?? {});
|
const selectedTag = computed(() => props.tagsById[tags.value.value[0]] ?? {});
|
||||||
|
|
||||||
function openExecutionsView() {
|
function openExecutionsView() {
|
||||||
@@ -198,13 +197,17 @@ function openExecutionsView() {
|
|||||||
</template>
|
</template>
|
||||||
</EvaluationStep>
|
</EvaluationStep>
|
||||||
</div>
|
</div>
|
||||||
<Modal ref="nodePinningModal" width="80vw" height="85vh" :name="NODE_PINNING_MODAL_KEY">
|
<Modal
|
||||||
|
width="calc(100% - (48px * 2))"
|
||||||
|
height="calc(100% - (48px * 2))"
|
||||||
|
:custom-class="$style.pinnigModal"
|
||||||
|
:name="NODE_PINNING_MODAL_KEY"
|
||||||
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<N8nHeading size="large" :bold="true">
|
<N8nHeading tag="h3" size="xlarge" color="text-dark" class="mb-2xs">
|
||||||
{{ locale.baseText('testDefinition.edit.selectNodes') }}
|
{{ locale.baseText('testDefinition.edit.selectNodes') }}
|
||||||
</N8nHeading>
|
</N8nHeading>
|
||||||
<br />
|
<N8nText color="text-base">
|
||||||
<N8nText>
|
|
||||||
{{ locale.baseText('testDefinition.edit.modal.description') }}
|
{{ locale.baseText('testDefinition.edit.modal.description') }}
|
||||||
</N8nText>
|
</N8nText>
|
||||||
</template>
|
</template>
|
||||||
@@ -216,6 +219,11 @@ function openExecutionsView() {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style module lang="scss">
|
<style module lang="scss">
|
||||||
|
.pinnigModal {
|
||||||
|
--dialog-max-width: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.nestedSteps {
|
.nestedSteps {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 20% 1fr;
|
grid-template-columns: 20% 1fr;
|
||||||
|
|||||||
@@ -94,21 +94,18 @@ describe('NodesPinning', () => {
|
|||||||
expect(container.querySelector('[data-node-name="Node 2"]')).toBeInTheDocument();
|
expect(container.querySelector('[data-node-name="Node 2"]')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update node classes when pinning/unpinning nodes', async () => {
|
it('should update UI when pinning/unpinning nodes', async () => {
|
||||||
const { container } = renderComponent();
|
const { container, getAllByTestId } = renderComponent();
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(container.querySelector('[data-node-name="Node 1"]')).toBeInTheDocument();
|
expect(container.querySelector('[data-node-name="Node 1"]')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
const buttons = getAllByTestId('node-pin-button');
|
||||||
expect(container.querySelector('[data-node-name="Node 1"]')).toHaveClass(
|
expect(buttons.length).toBe(2);
|
||||||
'canvasNode pinnedNode',
|
|
||||||
);
|
expect(buttons[0]).toHaveTextContent('Unpin');
|
||||||
expect(container.querySelector('[data-node-name="Node 2"]')).toHaveClass(
|
expect(buttons[1]).toHaveTextContent('Pin');
|
||||||
'canvasNode notPinnedNode',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should emit update:modelValue when pinning nodes', async () => {
|
it('should emit update:modelValue when pinning nodes', async () => {
|
||||||
|
|||||||
@@ -1,23 +1,42 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import ContextMenu from '@/components/ContextMenu/ContextMenu.vue';
|
||||||
|
import {
|
||||||
|
type CanvasLayoutEvent,
|
||||||
|
type CanvasLayoutSource,
|
||||||
|
useCanvasLayout,
|
||||||
|
} from '@/composables/useCanvasLayout';
|
||||||
|
import { useCanvasNodeHover } from '@/composables/useCanvasNodeHover';
|
||||||
|
import { useCanvasTraversal } from '@/composables/useCanvasTraversal';
|
||||||
|
import { type ContextMenuAction, useContextMenu } from '@/composables/useContextMenu';
|
||||||
|
import { useKeybindings } from '@/composables/useKeybindings';
|
||||||
|
import type { PinDataSource } from '@/composables/usePinnedData';
|
||||||
|
import { CanvasKey } from '@/constants';
|
||||||
|
import type { NodeCreatorOpenSource } from '@/Interface';
|
||||||
import {
|
import {
|
||||||
type CanvasConnection,
|
type CanvasConnection,
|
||||||
|
type CanvasEventBusEvents,
|
||||||
type CanvasNode,
|
type CanvasNode,
|
||||||
type CanvasNodeMoveEvent,
|
type CanvasNodeMoveEvent,
|
||||||
type CanvasEventBusEvents,
|
|
||||||
type ConnectStartEvent,
|
type ConnectStartEvent,
|
||||||
CanvasNodeRenderType,
|
CanvasNodeRenderType,
|
||||||
} from '@/types';
|
} from '@/types';
|
||||||
|
import { GRID_SIZE } from '@/utils/nodeViewUtils';
|
||||||
|
import { isPresent } from '@/utils/typesUtils';
|
||||||
|
import { useDeviceSupport } from '@n8n/composables/useDeviceSupport';
|
||||||
|
import { useShortKeyPress } from '@n8n/composables/useShortKeyPress';
|
||||||
|
import type { EventBus } from '@n8n/utils/event-bus';
|
||||||
|
import { createEventBus } from '@n8n/utils/event-bus';
|
||||||
import type {
|
import type {
|
||||||
Connection,
|
Connection,
|
||||||
XYPosition,
|
GraphNode,
|
||||||
NodeDragEvent,
|
NodeDragEvent,
|
||||||
NodeMouseEvent,
|
NodeMouseEvent,
|
||||||
GraphNode,
|
XYPosition,
|
||||||
} from '@vue-flow/core';
|
} from '@vue-flow/core';
|
||||||
import { useVueFlow, VueFlow, PanelPosition, MarkerType } from '@vue-flow/core';
|
import { MarkerType, PanelPosition, useVueFlow, VueFlow } from '@vue-flow/core';
|
||||||
import { MiniMap } from '@vue-flow/minimap';
|
import { MiniMap } from '@vue-flow/minimap';
|
||||||
import Node from './elements/nodes/CanvasNode.vue';
|
import { onKeyDown, onKeyUp, useThrottleFn } from '@vueuse/core';
|
||||||
import Edge from './elements/edges/CanvasEdge.vue';
|
import { NodeConnectionType } from 'n8n-workflow';
|
||||||
import {
|
import {
|
||||||
computed,
|
computed,
|
||||||
nextTick,
|
nextTick,
|
||||||
@@ -29,29 +48,10 @@ import {
|
|||||||
useCssModule,
|
useCssModule,
|
||||||
watch,
|
watch,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import type { EventBus } from '@n8n/utils/event-bus';
|
|
||||||
import { createEventBus } from '@n8n/utils/event-bus';
|
|
||||||
import { useDeviceSupport } from '@n8n/composables/useDeviceSupport';
|
|
||||||
import { useShortKeyPress } from '@n8n/composables/useShortKeyPress';
|
|
||||||
import { useContextMenu, type ContextMenuAction } from '@/composables/useContextMenu';
|
|
||||||
import { useKeybindings } from '@/composables/useKeybindings';
|
|
||||||
import ContextMenu from '@/components/ContextMenu/ContextMenu.vue';
|
|
||||||
import type { NodeCreatorOpenSource } from '@/Interface';
|
|
||||||
import type { PinDataSource } from '@/composables/usePinnedData';
|
|
||||||
import { isPresent } from '@/utils/typesUtils';
|
|
||||||
import { GRID_SIZE } from '@/utils/nodeViewUtils';
|
|
||||||
import { CanvasKey } from '@/constants';
|
|
||||||
import { onKeyDown, onKeyUp, useThrottleFn } from '@vueuse/core';
|
|
||||||
import CanvasArrowHeadMarker from './elements/edges/CanvasArrowHeadMarker.vue';
|
|
||||||
import CanvasBackground from './elements/background/CanvasBackground.vue';
|
import CanvasBackground from './elements/background/CanvasBackground.vue';
|
||||||
import { useCanvasTraversal } from '@/composables/useCanvasTraversal';
|
import CanvasArrowHeadMarker from './elements/edges/CanvasArrowHeadMarker.vue';
|
||||||
import { NodeConnectionType } from 'n8n-workflow';
|
import Edge from './elements/edges/CanvasEdge.vue';
|
||||||
import { useCanvasNodeHover } from '@/composables/useCanvasNodeHover';
|
import Node from './elements/nodes/CanvasNode.vue';
|
||||||
import {
|
|
||||||
type CanvasLayoutEvent,
|
|
||||||
type CanvasLayoutSource,
|
|
||||||
useCanvasLayout,
|
|
||||||
} from '@/composables/useCanvasLayout';
|
|
||||||
|
|
||||||
const $style = useCssModule();
|
const $style = useCssModule();
|
||||||
|
|
||||||
@@ -804,29 +804,31 @@ provide(CanvasKey, {
|
|||||||
@drop="onDrop"
|
@drop="onDrop"
|
||||||
>
|
>
|
||||||
<template #node-canvas-node="nodeProps">
|
<template #node-canvas-node="nodeProps">
|
||||||
<Node
|
<slot name="node" v-bind="{ nodeProps }">
|
||||||
v-bind="nodeProps"
|
<Node
|
||||||
:read-only="readOnly"
|
v-bind="nodeProps"
|
||||||
:event-bus="eventBus"
|
:read-only="readOnly"
|
||||||
:hovered="nodesHoveredById[nodeProps.id]"
|
:event-bus="eventBus"
|
||||||
:nearby-hovered="nodeProps.id === hoveredTriggerNode.id.value"
|
:hovered="nodesHoveredById[nodeProps.id]"
|
||||||
@delete="onDeleteNode"
|
:nearby-hovered="nodeProps.id === hoveredTriggerNode.id.value"
|
||||||
@run="onRunNode"
|
@delete="onDeleteNode"
|
||||||
@select="onSelectNode"
|
@run="onRunNode"
|
||||||
@toggle="onToggleNodeEnabled"
|
@select="onSelectNode"
|
||||||
@activate="onSetNodeActivated"
|
@toggle="onToggleNodeEnabled"
|
||||||
@deactivate="onSetNodeDeactivated"
|
@activate="onSetNodeActivated"
|
||||||
@open:contextmenu="onOpenNodeContextMenu"
|
@deactivate="onSetNodeDeactivated"
|
||||||
@update="onUpdateNodeParameters"
|
@open:contextmenu="onOpenNodeContextMenu"
|
||||||
@update:inputs="onUpdateNodeInputs"
|
@update="onUpdateNodeParameters"
|
||||||
@update:outputs="onUpdateNodeOutputs"
|
@update:inputs="onUpdateNodeInputs"
|
||||||
@move="onUpdateNodePosition"
|
@update:outputs="onUpdateNodeOutputs"
|
||||||
@add="onClickNodeAdd"
|
@move="onUpdateNodePosition"
|
||||||
>
|
@add="onClickNodeAdd"
|
||||||
<template v-if="$slots.nodeToolbar" #toolbar="toolbarProps">
|
>
|
||||||
<slot name="nodeToolbar" v-bind="toolbarProps" />
|
<template v-if="$slots.nodeToolbar" #toolbar="toolbarProps">
|
||||||
</template>
|
<slot name="nodeToolbar" v-bind="toolbarProps" />
|
||||||
</Node>
|
</template>
|
||||||
|
</Node>
|
||||||
|
</slot>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #edge-canvas-edge="edgeProps">
|
<template #edge-canvas-edge="edgeProps">
|
||||||
|
|||||||
@@ -2908,12 +2908,14 @@
|
|||||||
"testDefinition.edit.step.metrics.description": "The names of fields output by your evaluation workflow in the step above.",
|
"testDefinition.edit.step.metrics.description": "The names of fields output by your evaluation workflow in the step above.",
|
||||||
"testDefinition.edit.step.collapse": "Collapse",
|
"testDefinition.edit.step.collapse": "Collapse",
|
||||||
"testDefinition.edit.step.configure": "Configure",
|
"testDefinition.edit.step.configure": "Configure",
|
||||||
"testDefinition.edit.selectNodes": "Select nodes to mock",
|
"testDefinition.edit.selectNodes": "Pin nodes to mock them",
|
||||||
"testDefinition.edit.modal.description": "Choose which past data to keep when re-running the execution(s). Any mocked node will be replayed rather than re-executed. The trigger is always mocked.",
|
"testDefinition.edit.modal.description": "Choose which past data to keep when re-running the execution(s). Any mocked node will be replayed rather than re-executed. The trigger is always mocked.",
|
||||||
"testDefinition.edit.runExecution": "Run execution",
|
"testDefinition.edit.runExecution": "Run execution",
|
||||||
"testDefinition.edit.pastRuns": "Past runs",
|
"testDefinition.edit.pastRuns": "Past runs",
|
||||||
"testDefinition.edit.pastRuns.total": "No runs | Past run ({count}) | Past runs ({count})",
|
"testDefinition.edit.pastRuns.total": "No runs | Past run ({count}) | Past runs ({count})",
|
||||||
"testDefinition.edit.nodesPinning.pinButtonTooltip": "Use benchmark data for this node during evaluation execution",
|
"testDefinition.edit.nodesPinning.pinButtonTooltip": "Use benchmark data for this node during evaluation execution",
|
||||||
|
"testDefinition.edit.nodesPinning.pinButtonTooltip.pinned": "This node will not be re-executed",
|
||||||
|
"testDefinition.edit.nodesPinning.triggerTooltip": "Trigger nodes are mocked by default",
|
||||||
"testDefinition.edit.saving": "Saving...",
|
"testDefinition.edit.saving": "Saving...",
|
||||||
"testDefinition.edit.saved": "Test saved",
|
"testDefinition.edit.saved": "Test saved",
|
||||||
"testDefinition.list.testDeleted": "Test deleted",
|
"testDefinition.list.testDeleted": "Test deleted",
|
||||||
|
|||||||
Reference in New Issue
Block a user