n8n/packages/editor-ui/src/components/TestDefinition/EditDefinition/NodesPinning.vue

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

195 lines
5.2 KiB
Vue
Raw Normal View History

<script setup lang="ts">
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { computed, onMounted, ref, useCssModule } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useCanvasOperations } from '@/composables/useCanvasOperations';
import { useCanvasMapping } from '@/composables/useCanvasMapping';
import { createEventBus, N8nTooltip } from 'n8n-design-system';
import type { CanvasConnectionPort, CanvasEventBusEvents, CanvasNodeData } from '@/types';
import { useVueFlow } from '@vue-flow/core';
import { useI18n } from '@/composables/useI18n';
const workflowsStore = useWorkflowsStore();
const nodeTypesStore = useNodeTypesStore();
const route = useRoute();
const router = useRouter();
const locale = useI18n();
const { resetWorkspace, initializeWorkspace } = useCanvasOperations({ router });
const eventBus = createEventBus<CanvasEventBusEvents>();
const style = useCssModule();
const uuid = crypto.randomUUID();
const props = defineProps<{
modelValue: Array<{ name: string }>;
}>();
const emit = defineEmits<{
'update:modelValue': [value: Array<{ name: string }>];
}>();
const isLoading = ref(true);
const workflowId = computed(() => route.params.name as string);
const testId = computed(() => route.params.testId as string);
const workflow = computed(() => workflowsStore.getWorkflowById(workflowId.value));
const workflowObject = computed(() => workflowsStore.getCurrentWorkflow(true));
const canvasId = computed(() => `${uuid}-${testId.value}`);
const { onNodesInitialized, fitView, zoomTo } = useVueFlow({ id: canvasId.value });
const nodes = computed(() => {
return workflow.value.nodes ?? [];
});
const connections = computed(() => workflow.value.connections);
const { nodes: mappedNodes, connections: mappedConnections } = useCanvasMapping({
nodes,
connections,
workflowObject,
});
async function loadData() {
workflowsStore.resetState();
resetWorkspace();
const loadingPromise = Promise.all([
nodeTypesStore.getNodeTypes(),
workflowsStore.fetchWorkflow(workflowId.value),
]);
await loadingPromise;
initializeWorkspace(workflow.value);
disableAllNodes();
}
function getNodeNameById(id: string) {
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) => {
const matchedNode = mappedNodes.value.find(
(mappedNode) => mappedNode?.data?.name === node.name,
);
return matchedNode?.id ?? null;
})
.filter((n) => n !== null);
if (pinnedNodes.length > 0) {
updateNodeClasses(pinnedNodes, true);
}
}
function onPinButtonClick(data: CanvasNodeData) {
const nodeName = getNodeNameById(data.id);
if (!nodeName) return;
const isPinned = props.modelValue.some((node) => node.name === nodeName);
const updatedNodes = isPinned
? props.modelValue.filter((node) => node.name !== nodeName)
: [...props.modelValue, { name: nodeName }];
emit('update:modelValue', updatedNodes);
updateNodeClasses([data.id], !isPinned);
}
function isPinButtonVisible(outputs: CanvasConnectionPort[]) {
return outputs.length === 1;
}
onNodesInitialized(async () => {
await fitView();
isLoading.value = false;
await zoomTo(0.7, { duration: 400 });
});
onMounted(loadData);
</script>
<template>
<div :class="$style.container">
<N8nSpinner v-if="isLoading" size="xlarge" type="dots" :class="$style.spinner" />
<Canvas
:id="canvasId"
:loading="isLoading"
:class="{ [$style.canvas]: true }"
:nodes="mappedNodes"
:connections="mappedConnections"
:show-bug-reporting-button="false"
:read-only="true"
:event-bus="eventBus"
>
<template #nodeToolbar="{ data, outputs }">
<div :class="$style.pinButtonContainer">
<N8nTooltip v-if="isPinButtonVisible(outputs)" placement="left">
<template #content>
{{ locale.baseText('testDefinition.edit.nodesPinning.pinButtonTooltip') }}
</template>
<n8n-icon-button
type="tertiary"
size="large"
icon="thumbtack"
:class="$style.pinButton"
@click="onPinButtonClick(data)"
/>
</N8nTooltip>
</div>
</template>
</Canvas>
</div>
</template>
<style lang="scss" module>
.container {
width: 100vw;
height: 100%;
}
.pinButtonContainer {
position: absolute;
right: 0;
display: flex;
justify-content: flex-end;
bottom: 100%;
}
.pinButton {
cursor: pointer;
color: var(--canvas-node--border-color);
border: none;
}
.notPinnedNode,
.pinnedNode {
:global(.n8n-node-icon) > div {
filter: contrast(40%) brightness(1.5) grayscale(100%);
}
}
.pinnedNode {
--canvas-node--border-color: hsla(247, 49%, 55%, 1);
:global(.n8n-node-icon) > div {
filter: contrast(40%) brightness(1.5) grayscale(100%);
}
}
.spinner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
</style>