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:
Raúl Gómez Morales
2025-03-17 09:40:07 +01:00
committed by GitHub
parent c91688d494
commit 4a1e5798ff
5 changed files with 198 additions and 174 deletions

View File

@@ -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);
} }

View File

@@ -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;

View File

@@ -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 () => {

View File

@@ -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">

View File

@@ -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",